diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 5c1c453dd..f6c1fd80c 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -11,11 +11,6 @@ runs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Uncomment build epoch - shell: bash - run: | - sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - - name: Install dependencies shell: bash run: | @@ -23,7 +18,7 @@ runs: sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x cache: pip diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 815b03c86..562ce00f9 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -371,7 +371,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -440,7 +440,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -495,7 +495,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 13d3d1b4e..d8ff6e631 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -31,7 +31,7 @@ jobs: repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index 93114e2c7..5fca90961 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check for PR labels - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 786feeced..4e285852d 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -177,7 +177,7 @@ jobs: - name: Comment test results on PR if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index ccd99e792..486f4b1a6 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -63,7 +63,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5a11fdfa8..32e2c2c8b 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: days-before-stale: 45 exempt-issue-labels: pinned,3.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52f180aa2..942659348 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: pio upgrade - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 22 diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml index 2d191fc44..51082fc5f 100644 --- a/.github/workflows/trunk_format_pr.yml +++ b/.github/workflows/trunk_format_pr.yml @@ -39,7 +39,7 @@ jobs: git push - name: Comment on PR - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index de38e3ec0..c1fde9602 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,28 +4,28 @@ cli: plugins: sources: - id: trunk - ref: v1.7.1 + ref: v1.7.2 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.461 - - renovate@41.74.0 + - checkov@3.2.471 + - renovate@41.115.2 - prettier@3.6.2 - - trufflehog@3.90.5 + - trufflehog@3.90.6 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.64.1 - - taplo@0.9.3 - - ruff@0.12.7 + - trivy@0.66.0 + - taplo@0.10.0 + - ruff@0.13.0 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 - svgo@4.0.0 - actionlint@1.7.7 - flake8@7.3.0 - - hadolint@2.12.1-beta + - hadolint@2.13.1 - shfmt@3.6.0 - - shellcheck@0.10.0 + - shellcheck@0.11.0 - black@25.1.0 - git-diff-check - gitleaks@8.28.0 diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 20b3f8e3d..95c3bf3d9 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip + https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip framework = arduino build_src_filter = @@ -31,6 +31,8 @@ lib_deps = 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 adafruit/Adafruit seesaw Library@1.7.9 + # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip build_flags = ${arduino_base.build_flags} diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index d91607a7d..8b7d256b3 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -50,7 +50,7 @@ lib_deps = ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main - https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip + https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip lib_ignore = OneButton diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index fdd7caa11..7bd19aaa9 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - export PIP_BREAK_SYSTEM_PACKAGES=1 if (echo $2 | grep -q "esp32"); then diff --git a/bin/device-install.bat b/bin/device-install.bat index 12bfd4f6e..9c206d718 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -7,6 +7,7 @@ SET "DEBUG=0" SET "PYTHON=" SET "TFT_BUILD=0" SET "BIGDB8=0" +SET "MUIDB8=0" SET "BIGDB16=0" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" @@ -14,11 +15,12 @@ SET "LOGCOUNTER=0" SET "BPS_RESET=0" @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. -SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone" +SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv" SET "C3=esp32c3" @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. -SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" -SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3" +SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" +SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator" +SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv" GOTO getopts :help @@ -100,7 +102,6 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( @@ -120,11 +121,10 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." @@ -142,7 +142,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %BPS_RESET% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status GOTO eof ) @@ -164,6 +164,15 @@ FOR %%a IN (%BIGDB_8MB%) DO ( ) :end_loop_bigdb_8mb +FOR %%a IN (%MUIDB_8MB%) DO ( + IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( + @REM We are working with any of %MUIDB_8MB%. + SET "MUIDB8=1" + GOTO end_loop_muidb_8mb + ) +) +:end_loop_muidb_8mb + FOR %%a IN (%BIGDB_16MB%) DO ( IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( @REM We are working with any of %BIGDB_16MB%. @@ -174,6 +183,7 @@ FOR %%a IN (%BIGDB_16MB%) DO ( :end_loop_bigdb_16mb IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected." +IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected." IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected." @REM Extract BASENAME from %FILENAME% for later use. @@ -218,6 +228,12 @@ IF %BIGDB8% EQU 1 ( SET "SPIFFS_OFFSET=0x670000" ) +@REM Offsets for MUIDB 8mb. +IF %MUIDB8% EQU 1 ( + SET "OTA_OFFSET=0x5D0000" + SET "SPIFFS_OFFSET=0x670000" +) + @REM Offsets for BigDB 16mb. IF %BIGDB16% EQU 1 ( SET "OTA_OFFSET=0x650000" diff --git a/bin/device-install.sh b/bin/device-install.sh index 4674113b6..594f9dd6b 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -5,38 +5,43 @@ BPS_RESET=false TFT_BUILD=false MCU="" +# Constants +RESET_BAUD=1200 +FIRMWARE_OFFSET=0x00 + # Variant groups BIGDB_8MB=( - "picomputer-s3" - "unphone" - "seeed-sensecap-indicator" - "crowpanel-esp32s3" - "heltec_capsule_sensor_v3" - "heltec-v3" - "heltec-vision-master-e213" - "heltec-vision-master-e290" - "heltec-vision-master-t190" - "heltec-wireless-paper" - "heltec-wireless-tracker" - "heltec-wsl-v3" - "icarus" - "seeed-xiao-s3" - "tbeam-s3-core" - "tracksenger" + "crowpanel-esp32s3" + "heltec_capsule_sensor_v3" + "heltec-v3" + "heltec-vision-master-e213" + "heltec-vision-master-e290" + "heltec-vision-master-t190" + "heltec-wireless-paper" + "heltec-wireless-tracker" + "heltec-wsl-v3" + "icarus" + "seeed-xiao-s3" + "tbeam-s3-core" + "tracksenger" +) +MUIDB_8MB=( + "picomputer-s3" + "unphone" + "seeed-sensecap-indicator" ) BIGDB_16MB=( - "t-deck" - "mesh-tab" - "t-energy-s3" - "dreamcatcher" - "ESP32-S3-Pico" - "m5stack-cores3" - "station-g2" + "t-deck" + "mesh-tab" + "t-energy-s3" + "dreamcatcher" + "ESP32-S3-Pico" + "m5stack-cores3" + "station-g2" "t-eth-elite" + "tlora-pager" "t-watch-s3" - "elecrow-adv-35-tft" - "elecrow-adv-24-28-tft" - "elecrow-adv1-43-50-70-tft" + "elecrow-adv" ) S3_VARIANTS=( "s3" @@ -47,6 +52,7 @@ S3_VARIANTS=( "station-g2" "unphone" "t-eth-elite" + "tlora-pager" "mesh-tab" "dreamcatcher" "ESP32-S3-Pico" @@ -106,8 +112,8 @@ while [ $# -gt 0 ]; do shift ;; --1200bps-reset) - BPS_RESET=true - ;; + BPS_RESET=true + ;; --) # Stop parsing options shift break @@ -121,7 +127,7 @@ while [ $# -gt 0 ]; do done if [[ $BPS_RESET == true ]]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -158,6 +164,13 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then fi done + for variant in "${MUIDB_8MB[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + OFFSET=0x670000 + OTA_OFFSET=0x5D0000 + fi + done + # littlefs* offset for BigDB 16mb and OTA OFFSET. for variant in "${BIGDB_16MB[@]}"; do if [ -z "${FILENAME##*"$variant"*}" ]; then @@ -201,8 +214,8 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then fi echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase_flash - $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" + $ESPTOOL_CMD erase-flash + $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" diff --git a/bin/device-update.bat b/bin/device-update.bat index 6d55294a7..a263da992 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -6,6 +6,8 @@ SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" SET "ESPTOOL_BAUD=115200" +SET "RESET_BAUD=1200" +SET "UPDATE_OFFSET=0x10000" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "CHANGE_MODE=0" @@ -85,14 +87,13 @@ IF "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( - SET "ESPTOOL_CMD=!PYTHON! -m esptool" + SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( - CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool... + CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. @@ -105,11 +106,11 @@ IF NOT "__%PYTHON%__"=="____" ( CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." !ESPTOOL_CMD! >nul 2>&1 -IF %ERRORLEVEL% GEQ 2 ( - @REM esptool exits with code 1 if help is displayed. +CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" +IF %ERRORLEVEL% EQU 9009 ( + @REM 9009 = command not found on Windows CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" EXIT /B 1 - GOTO eof ) IF %DEBUG% EQU 1 ( CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." @@ -127,13 +128,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %CHANGE_MODE% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status GOTO eof ) @REM Flashing operations. -CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof +CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -145,9 +146,9 @@ EXIT /B %ERRORLEVEL% :RUN_ESPTOOL @REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Also handles %ERRORLEVEL%. -@REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename] +@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM. -@REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin" +@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 diff --git a/bin/device-update.sh b/bin/device-update.sh index 2196d3af9..6f29496e9 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -3,6 +3,11 @@ PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false +# Constants +FLASH_BAUD=115200 +RESET_BAUD=1200 +UPDATE_OFFSET=0x10000 + # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then ESPTOOL_CMD="$PYTHON -m esptool" @@ -64,7 +69,7 @@ done shift "$((OPTIND-1))" if [ "$CHANGE_MODE" = true ]; then - $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + $ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status exit 0 fi @@ -75,7 +80,7 @@ fi if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then echo "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}" + $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" else show_help echo "Invalid file: ${FILENAME}" diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index f3b3bb14d..108ca4910 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,15 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index fc1b4bc2e..e54d1586f 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -6,6 +6,8 @@ from os.path import join import subprocess import json import re +import time +from datetime import datetime from readprops import readProps @@ -125,11 +127,16 @@ for pref in userPrefs: pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") # General options that are passed to the C and C++ compilers +# Calculate unix epoch for current day (midnight) +current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) +build_epoch = int(current_date.timestamp()) + flags = [ "-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_REPO=" + repo_owner, + "-DBUILD_EPOCH=" + str(build_epoch), ] + pref_flags print ("Using flags:") diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json new file mode 100644 index 000000000..9e551c082 --- /dev/null +++ b/boards/heltec_mesh_solar.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x0071"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_solar", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/project/meshsolar/", + "vendor": "Heltec" +} diff --git a/boards/seeed-sensecap-indicator.json b/boards/seeed-sensecap-indicator.json index 03bff35b5..37a97cdf1 100644 --- a/boards/seeed-sensecap-indicator.json +++ b/boards/seeed-sensecap-indicator.json @@ -2,7 +2,7 @@ "build": { "arduino": { "ldscript": "esp32s3_out.ld", - "partitions": "default_8MB.csv", + "partitions": "partition-table-8MB.csv", "memory_type": "qio_opi" }, "core": "esp32", diff --git a/boards/unphone.json b/boards/unphone.json index bf711993c..4d37f7bb5 100644 --- a/boards/unphone.json +++ b/boards/unphone.json @@ -3,7 +3,7 @@ "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi", - "partitions": "default_8MB.csv" + "partitions": "partition-table-8MB.csv" }, "core": "esp32", "extra_flags": [ diff --git a/boards/wiscore_rak3172.json b/boards/wiscore_rak3172.json index 714e09115..69ee506b4 100644 --- a/boards/wiscore_rak3172.json +++ b/boards/wiscore_rak3172.json @@ -5,7 +5,7 @@ }, "core": "stm32", "cpu": "cortex-m4", - "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX", + "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "f_cpu": "48000000L", "mcu": "stm32wle5ccu", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", diff --git a/debian/changelog b/debian/changelog index b36a22168..29841d0db 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.6.0) UNRELEASED; urgency=medium +meshtasticd (2.7.9.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -39,5 +39,12 @@ meshtasticd (2.7.6.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Tue, 12 Aug 2025 23:48:48 +0000 + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + -- Wed, 03 Sep 2025 23:39:17 +0000 diff --git a/partition-table-8MB.csv b/partition-table-8MB.csv new file mode 100644 index 000000000..0bfbc22ba --- /dev/null +++ b/partition-table-8MB.csv @@ -0,0 +1,7 @@ +# This is a layout for 8MB of flash for MUI devices +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x5C0000, +flashApp, app, ota_1, 0x5D0000,0x0A0000, +spiffs, data, spiffs, 0x670000,0x180000 \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index cce4d2dcf..e2eb55dce 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,14 +53,14 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage - #-DBUILD_EPOCH=$UNIX_TIME + #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master @@ -118,7 +118,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/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip + https://github.com/meshtastic/device-ui/archive/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -157,8 +157,8 @@ lib_deps = emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library adafruit/Adafruit MLX90614 Library@2.1.5 - # renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221 - https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip + # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 + https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU @@ -177,6 +177,8 @@ lib_deps = adafruit/Adafruit PCT2075@1.0.5 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 + # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 + adafruit/Adafruit TSL2561@1.1.2 ; (not included in native / portduino) [environmental_extra] diff --git a/protobufs b/protobufs index 8985852d7..945b796a9 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 8985852d752de3f7210f9a4a3e0923120ec438b3 +Subproject commit 945b796a982f38171a9e0d28b5c8b1f7d53c5cd1 diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index e4ef3b443..947b1e054 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -183,9 +183,9 @@ class AmbientLightingThread : public concurrency::OSThread #endif #endif pixels.show(); - LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index a34710eb0..98bbe0f72 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -2,6 +2,12 @@ #include "configuration.h" +// Forward declarations +#if defined(DEBUG_HEAP) +class MemGet; +extern MemGet memGet; +#endif + // DEBUG LED #ifndef LED_STATE_ON #define LED_STATE_ON 1 @@ -23,6 +29,7 @@ #define MESHTASTIC_LOG_LEVEL_ERROR "ERROR" #define MESHTASTIC_LOG_LEVEL_CRIT "CRIT " #define MESHTASTIC_LOG_LEVEL_TRACE "TRACE" +#define MESHTASTIC_LOG_LEVEL_HEAP "HEAP" #include "SerialConsole.h" @@ -62,6 +69,25 @@ #endif #endif +#if defined(DEBUG_HEAP) +#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__) + +// Macro-based heap debugging +#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); +#define DEBUG_HEAP_AFTER(context, ptr) \ + do { \ + auto heapAfter = memGet.getFreeHeap(); \ + if (heapBefore != heapAfter) { \ + LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \ + } \ + } while (0) + +#else +#define LOG_HEAP(...) +#define DEBUG_HEAP_BEFORE +#define DEBUG_HEAP_AFTER(context, ptr) +#endif + /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic extern "C" void logLegacy(const char *level, const char *fmt, ...); diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 44bc0897b..d367aa661 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,7 +1,14 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName) +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset) { + + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } + switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return useShortName ? "ShortT" : "ShortTurbo"; diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index f8ccfcbb6..2d7a3e8db 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -4,5 +4,6 @@ class DisplayFormatters { public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName); + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset); }; diff --git a/src/Power.cpp b/src/Power.cpp index 8a16132f1..7de82b8d6 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -128,6 +128,7 @@ RAK9154Sensor rak9154Sensor; #ifdef HAS_PPM // note: XPOWERS_CHIP_XXX must be defined in variant.h #include +XPowersPPM *PPM = NULL; #endif #ifdef HAS_BQ27220 @@ -681,6 +682,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (meshSolarInit()) { + found = true; } else if (analogInit()) { found = true; } @@ -743,7 +746,11 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { +#ifdef T_DECK_PRO + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button +#else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen +#endif } #endif #if !defined(ARCH_STM32WL) @@ -761,7 +768,7 @@ void Power::shutdown() #ifdef PIN_LED3 ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, false, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) exit(EXIT_SUCCESS); #else @@ -826,18 +833,27 @@ void Power::readPowerStatus() newStatus.notifyObservers(&powerStatus2); #ifdef DEBUG_HEAP if (lastheap != memGet.getFreeHeap()) { - std::string threadlist = "Threads running:"; + // Use stack-allocated buffer to avoid heap allocations in monitoring code + char threadlist[256] = "Threads running:"; + int threadlistLen = strlen(threadlist); int running = 0; for (int i = 0; i < MAX_THREADS; i++) { auto thread = concurrency::mainController.get(i); if ((thread != nullptr) && (thread->enabled)) { - threadlist += vformat(" %s", thread->ThreadName.c_str()); + // Use snprintf to safely append to stack buffer without heap allocation + int remaining = sizeof(threadlist) - threadlistLen - 1; + if (remaining > 0) { + int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str()); + if (written > 0 && written < remaining) { + threadlistLen += written; + } + } running++; } } - LOG_DEBUG(threadlist.c_str()); - LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), - memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); + LOG_HEAP(threadlist); + LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), + memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); lastheap = memGet.getFreeHeap(); } #ifdef DEBUG_HEAP_MQTT @@ -849,15 +865,19 @@ void Power::readPowerStatus() sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); auto newHeap = memGet.getFreeHeap(); - std::string heapTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac); - std::string heapString = std::to_string(newHeap); - mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false); + // Use stack-allocated buffers to avoid heap allocations in monitoring code + char heapTopic[128]; + snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char heapString[16]; + snprintf(heapString, sizeof(heapString), "%u", newHeap); + mqtt->pubSub.publish(heapTopic, heapString, false); + auto wifiRSSI = WiFi.RSSI(); - std::string wifiTopic = - (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac); - std::string wifiString = std::to_string(wifiRSSI); - mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false); + char wifiTopic[128]; + snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); + char wifiString[16]; + snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); + mqtt->pubSub.publish(wifiTopic, wifiString, false); } #endif @@ -1318,7 +1338,6 @@ bool Power::lipoInit() class LipoCharger : public HasBatteryLevel { private: - XPowersPPM *ppm = nullptr; BQ27220 *bq = nullptr; public: @@ -1327,41 +1346,41 @@ class LipoCharger : public HasBatteryLevel */ bool runOnce() { - if (ppm == nullptr) { - ppm = new XPowersPPM; - bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); // Set the minimum operating voltage. Below this voltage, the PPM will protect - // ppm->setSysPowerDownVoltage(3100); + // PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA - // ppm->setInputCurrentLimit(800); + // PPM->setInputCurrentLimit(800); // Disable current limit pin - // ppm->disableCurrentLimitPin(); + // PPM->disableCurrentLimitPin(); // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - ppm->setChargeTargetVoltage(4288); + PPM->setChargeTargetVoltage(4288); // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // ppm->setPrechargeCurr(64); + // PPM->setPrechargeCurr(64); // The premise is that limit pin is disabled, or it will // only follow the maximum charging current set by limit pin. // Set the charging current , Range:0~5056mA ,step:64mA - ppm->setChargerConstantCurr(1024); + PPM->setChargerConstantCurr(1024); // To obtain voltage data, the ADC must be enabled first - ppm->enableMeasure(); + PPM->enableMeasure(); // Turn on charging function // If there is no battery connected, do not turn on the charging function - ppm->enableCharge(); + PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); - delete ppm; - ppm = nullptr; + delete PPM; + PPM = nullptr; return false; } } @@ -1402,23 +1421,23 @@ class LipoCharger : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; } + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; } + virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } /** * return true if the battery is currently charging */ virtual bool isCharging() override { - bool isCharging = ppm->isCharging(); + bool isCharging = PPM->isCharging(); if (isCharging) { LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); } else { - if (!ppm->isVbusIn()) { + if (!PPM->isVbusIn()) { LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); } } @@ -1450,3 +1469,73 @@ bool Power::lipoChargerInit() return false; } #endif + +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" + +/** + * meshSolar class for an SMBUS battery sensor. + */ +class meshSolarBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + meshSolarStart(); + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } +}; + +meshSolarBatteryLevel meshSolarLevel; + +/** + * Init the meshSolar battery level sensor + */ +bool Power::meshSolarInit() +{ + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; +} + +#else +/** + * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::meshSolarInit() +{ + return false; +} +#endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 68c41980d..093a24678 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con int32_t SerialConsole::runOnce() { +#ifdef HELTEC_MESH_SOLAR + //After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. + if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port + && moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) + { + return 250; + } +#endif return runOncePart(); } diff --git a/src/concurrency/OSThread.cpp b/src/concurrency/OSThread.cpp index d9bb901b2..5aee03bbf 100644 --- a/src/concurrency/OSThread.cpp +++ b/src/concurrency/OSThread.cpp @@ -86,9 +86,9 @@ void OSThread::run() #ifdef DEBUG_HEAP auto newHeap = memGet.getFreeHeap(); if (newHeap < heap) - LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); + LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); if (heap < newHeap) - LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); + LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); #endif runned(); diff --git a/src/configuration.h b/src/configuration.h index 0e24990b5..81632c89e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,10 +26,10 @@ along with this program. If not, see . #include -#ifdef RV3028_RTC +#if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#ifdef PCF8563_RTC +#if __has_include("pcf8563.h") #include "pcf8563.h" #endif @@ -135,7 +135,7 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c1358861b..470a416c0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -79,7 +79,9 @@ class ScanI2C BQ27220, LTR553ALS, BHI260AP, - BMM150 + BMM150, + TSL2561, + DRV2605 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8b3670cd9..01a630b52 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -294,6 +294,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = AHT10; break; #endif +#if !defined(M5STACK_UNITC6L) case INA_ADDR: case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: @@ -340,6 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) // else: probably a RAK12500/UBLOX GPS on I2C } break; +#endif case MCP9808_ADDR: // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. @@ -461,7 +463,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address); + case TSL25911_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + if (registerValue == 0x50) { + type = TSL2591; + logFoundDevice("TSL25911", (uint8_t)addr.address); + } else { + type = TSL2561; + logFoundDevice("TSL2561", (uint8_t)addr.address); + } + break; + SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); @@ -483,8 +495,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } } break; diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ae74f0fe2..a663f46c4 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1,5 +1,4 @@ #include // Include for strstr -#include #include #include "configuration.h" @@ -843,9 +842,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) setPowerPMU(true); // Power (PMU): on writePinStandby(false); // Standby (pin): awake (not standby) setPowerUBLOX(true); // Standby (UBLOX): awake -#ifdef GNSS_AIROHA - lastFixStartMsec = 0; -#endif break; case GPS_SOFTSLEEP: @@ -863,9 +859,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep (not awake) setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; @@ -877,9 +871,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; } @@ -1062,6 +1054,8 @@ void GPS::down() } // If update interval long enough (or softsleep unsupported): hardsleep instead setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; } } @@ -1121,11 +1115,19 @@ int32_t GPS::runOnce() shouldPublish = true; } + uint8_t prev_fixQual = fixQual; bool gotLoc = lookForLocation(); if (gotLoc && !hasValidLocation) { // declare that we have location ASAP LOG_DEBUG("hasValidLocation RISING EDGE"); hasValidLocation = true; shouldPublish = true; + // Hold for 20secs after getting a lock to download ephemeris etc + fixHoldEnds = millis() + 20000; + } + + if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on. + fixHoldEnds = millis() + 20000; + shouldPublish = true; // Publish immediately, since next publish is at end of hold } bool tooLong = scheduling.searchedTooLong(); @@ -1134,8 +1136,7 @@ int32_t GPS::runOnce() // Once we get a location we no longer desperately want an update if ((gotLoc && gotTime) || tooLong) { - - if (tooLong) { + if (tooLong && !gotLoc) { // we didn't get a location during this ack window, therefore declare loss of lock if (hasValidLocation) { LOG_DEBUG("hasValidLocation FALLING EDGE"); @@ -1143,9 +1144,15 @@ int32_t GPS::runOnce() p = meshtastic_Position_init_default; hasValidLocation = false; } - - down(); - shouldPublish = true; // publish our update for this just finished acquisition window + if (millis() > fixHoldEnds) { + shouldPublish = true; // publish our update at the end of the lock hold + publishUpdate(); + down(); +#ifdef GPS_DEBUG + } else { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); +#endif + } } // If state has changed do a publish @@ -1198,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected"; LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ clearBuffer(); \ _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ return detectedDriver; \ } \ @@ -1360,36 +1367,55 @@ GnssModel_t GPS::probe(int serialSpeed) return GNSS_MODEL_UNKNOWN; } -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap) +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { - String response = ""; + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; + + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate + uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { if (_serial_gps->available()) { - response += (char)_serial_gps->read(); + char c = _serial_gps->read(); - if (response.endsWith(",") || response.endsWith("\r\n")) { + // Add char to buffer if there's space + if (responseLen < bufferSize - 1) { + response[responseLen++] = c; + response[responseLen] = '\0'; + } + + if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { #ifdef GPS_DEBUG - LOG_DEBUG(response.c_str()); + LOG_DEBUG(response); #endif // check if we can see our chips for (const auto &chipInfo : responseMap) { - if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) { + if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { LOG_INFO("%s detected", chipInfo.chipName.c_str()); + delete[] response; // Cleanup before return return chipInfo.driver; } } } - if (response.endsWith("\r\n")) { - response.trim(); - response = ""; // Reset the response string for the next potential message + if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { + // Reset the response buffer for the next potential message + responseLen = 0; + response[0] = '\0'; } } } #ifdef GPS_DEBUG - LOG_DEBUG(response.c_str()); + LOG_DEBUG(response); #endif - return GNSS_MODEL_UNKNOWN; // Return empty string on timeout + delete[] response; // Cleanup before return + return GNSS_MODEL_UNKNOWN; // Return unknown on timeout } GPS *GPS::createGps() @@ -1504,28 +1530,10 @@ static int32_t toDegInt(RawDegrees d) * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations * - * @return true if we've acquired a new location + * @return true if we've set a new time */ bool GPS::lookForTime() { - -#ifdef GNSS_AIROHA - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } -#endif auto ti = reader.time; auto d = reader.date; if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed @@ -1542,13 +1550,13 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s t.tm_year = d.year() - 1900; t.tm_isdst = false; if (t.tm_mon > -1) { - LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, - t.tm_sec, ti.age()); - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { - // Clear the GPS buffer if we got an invalid time - clearBuffer(); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, + t.tm_min, t.tm_sec, ti.age()); + return true; + } else { + return false; } - return true; } else return false; } else @@ -1563,25 +1571,6 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s */ bool GPS::lookForLocation() { -#ifdef GNSS_AIROHA - if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) { - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } - } -#endif // By default, TinyGPS++ does not parse GPGSA lines, which give us // the 2D/3D fixType (see NMEAGPS.h) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 9be57017f..46701f611 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -159,7 +159,7 @@ class GPS : private concurrency::OSThread uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0; + uint32_t fixHoldEnds = 0; uint32_t rx_gpio = 0; uint32_t tx_gpio = 0; @@ -236,7 +236,7 @@ class GPS : private concurrency::OSThread virtual int32_t runOnce() override; - GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); // Get GNSS model GnssModel_t probe(int serialSpeed); diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index d574c9ad0..da20e28eb 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -9,6 +9,9 @@ static RTCQuality currentQuality = RTCQualityNone; uint32_t lastSetFromPhoneNtpOrGps = 0; +static uint32_t lastTimeValidationWarning = 0; +static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds + RTCQuality getRTCQuality() { return currentQuality; @@ -23,7 +26,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -void readFromRTC() +RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC @@ -44,15 +47,25 @@ void readFromRTC() t.tm_sec = rtc.getSecond(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #elif defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { @@ -75,15 +88,26 @@ void readFromRTC() t.tm_sec = tc.second; tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #else if (!gettimeofday(&tv, NULL)) { @@ -92,8 +116,10 @@ void readFromRTC() LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; } #endif + return RTCSetResultNotSet; } /** @@ -101,7 +127,7 @@ void readFromRTC() * * @param q The quality of the provided time. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. - * @return True if the RTC was set, false otherwise. + * @return RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ @@ -112,7 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -230,7 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } + return RTCSetResultInvalidTime; + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -290,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - setenv("TZ", "GMT0", 1); - time_t res = mktime(tm); - if (*config.device.tzdef) { - setenv("TZ", config.device.tzdef, 1); - } else { - setenv("TZ", "UTC0", 1); + time_t result = 0; + + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + 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 + + // 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)) { + days_this_year_before_this_month += 1; } - return res; + + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; + + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; + + return result; #else return mktime(tm); #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 96dec575b..eca17bf35 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -48,10 +48,13 @@ uint32_t getTime(bool local = false); /// Return time since 1970 in secs. If quality is RTCQualityNone return zero uint32_t getValidTime(RTCQuality minQuality, bool local = false); -void readFromRTC(); +RTCSetResult readFromRTC(); time_t gm_mktime(struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 +#ifdef BUILD_EPOCH +static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow +#endif diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 1c9f290b6..c0c09cc27 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -67,20 +67,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) // FIXME - only draw bits have changed (use backbuf similar to the other displays) const bool flipped = config.display.flip_screen; + // HACK for L1 EInk +#if defined(SEEED_WIO_TRACKER_L1_EINK) + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } + } +#else for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); - - // Handle flip here, rather than with setRotation(), - // Avoids issues when display width is not a multiple of 8 if (flipped) adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); else adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } +#endif // Trigger the refresh in GxEPD2 LOG_DEBUG("Update E-Paper"); @@ -235,7 +243,7 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } -#elif defined(HELTEC_MESH_POCKET) +#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) { spi1 = &SPI1; spi1->begin(); @@ -249,6 +257,7 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index b840ce9ba..b4cee81fe 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..0a2229d0e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -317,8 +317,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_SPISSD1306) + dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); + if (!dispdev->init()) { + LOG_DEBUG("Error: SSD1306 not detected!"); + } else { + static_cast(dispdev)->setHorizontalOffset(32); + LOG_INFO("SSD1306 init success"); + } #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) @@ -507,7 +515,7 @@ void Screen::setup() // === Apply loaded brightness === #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); -#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) dispdev->setBrightness(brightness); #endif LOG_INFO("Applied screen brightness: %d", brightness); @@ -550,11 +558,11 @@ void Screen::setup() #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); -#else +#elif !defined(M5STACK_UNITC6L) dispdev->flipScreenVertically(); #endif } @@ -692,7 +700,11 @@ int32_t Screen::runOnce() #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { +#if defined(M5STACK_UNITC6L) + menuHandler::LoraRegionPicker(); +#else menuHandler::OnboardMessage(); +#endif } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { @@ -890,8 +902,12 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) fsi.positions.clock = numframes; +#if defined(M5STACK_UNITC6L) + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; +#else normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; +#endif indicatorIcons.push_back(digital_icon_clock); #endif @@ -1226,6 +1242,10 @@ void Screen::handleShowNextFrame() void Screen::setFastFramerate() { +#if defined(M5STACK_UNITC6L) + dispdev->clear(); + dispdev->display(); +#endif // We are about to start a transition so speed up fps targetFramerate = SCREEN_TRANSITION_FRAMERATE; @@ -1297,13 +1317,23 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) } } else { if (longName && longName[0]) { +#if defined(M5STACK_UNITC6L) + strcpy(banner, "New Message"); +#else snprintf(banner, sizeof(banner), "New Message from\n%s", longName); +#endif + } else { strcpy(banner, "New Message"); } } - +#if defined(M5STACK_UNITC6L) + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + playLongBeep(); +#else screen->showSimpleBanner(banner, 3000); +#endif } } @@ -1386,7 +1416,11 @@ int Screen::handleInputEvent(const InputEvent *event) if (devicestate.rx_text_message.from) { menuHandler::messageResponseMenu(); } else { +#if defined(M5STACK_UNITC6L) + menuHandler::textMessageMenu(); +#else menuHandler::textMessageBaseMenu(); +#endif } } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..ecc39ac60 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -81,6 +81,8 @@ class Screen #include #elif defined(USE_ST7789) #include +#elif defined(USE_SPISSD1306) +#include #else // the SH1106/SSD1306 variant is auto-detected #include diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 84ec45977..c497a27b2 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -73,12 +73,16 @@ #endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 +#elif defined(M5STACK_UNITC6L) +#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 +#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13 +#define FONT_LARGE FONT_SMALL_LOCAL // Height: 13 #else #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index b458e54e4..13691665a 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -124,7 +124,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; - +#if !defined(M5STACK_UNITC6L) // === Battery Icons === if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; @@ -337,7 +337,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } } } - +#endif display->setColor(WHITE); // Reset for other UI } diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index f8787612f..37ea9b94a 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device static LGFX *tft = nullptr; +#elif defined(ST7796_CS) +#include // Graphics and font library for ST7796 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) +#ifdef TFT_DUMMY_READ_PIXELS + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout +#else + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + _panel_instance.config(cfg); + } + +#ifdef ST7796_BL + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX *tft = nullptr; + #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #include // Graphics and font library for ILI9341/ILI9342 driver chip @@ -997,8 +1082,9 @@ static LGFX *tft = nullptr; #endif -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ + defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ + (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -1042,37 +1128,111 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #endif } +TFTDisplay::~TFTDisplay() +{ + // Clean up allocated line pixel buffer to prevent memory leak + if (linePixelBuffer != nullptr) { + free(linePixelBuffer); + linePixelBuffer = nullptr; + } +} + // Write the buffer to the display memory void TFTDisplay::display(bool fromBlank) { if (fromBlank) tft->fillScreen(TFT_BLACK); - // tft->clear(); + concurrency::LockGuard g(spiLock); - uint16_t x, y; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - for (y = 0; y < displayHeight; y++) { - for (x = 0; x < displayWidth; x++) { - auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); + + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent - auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); - if (isset != dblbuf_isset) { - tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; } - } else if (isset) { - tft->drawPixel(x, y, TFT_MESH); + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; + } + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; } } + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } + } + + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { + + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; + + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; + + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } + + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, + &linePixelBuffer[x_FirstPixelUpdate]); + + somethingChanged = true; + } + y++; } // Copy the Buffer to the Back Buffer - for (y = 0; y < (displayHeight / 8); y++) { - for (x = 0; x < displayWidth; x++) { - uint16_t pos = x + y * displayWidth; - buffer_back[pos] = buffer[pos]; - } - } + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); } void TFTDisplay::sdlLoop() @@ -1264,13 +1424,21 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation -#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) +#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft->fillScreen(TFT_BLACK); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; + } + } return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 60adfdf7c..a64922d23 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -20,6 +20,9 @@ class TFTDisplay : public OLEDDisplay */ TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); + // Destructor to clean up allocated memory + ~TFTDisplay(); + // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); @@ -58,4 +61,6 @@ class TFTDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; + + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5d9b5a33b..6137ddef8 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); @@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); @@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 } else { // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); @@ -261,12 +263,6 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->drawString(x + 1, y, "USB"); } - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; @@ -281,12 +277,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); // Line 1 (Still) +#if !defined(M5STACK_UNITC6L) display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); if (config.display.heading_bold) display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->setColor(WHITE); - +#endif // Setup string to assemble analogClock string std::string analogClock = ""; @@ -390,17 +387,24 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char shortnameble[35]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); +#if defined(M5STACK_UNITC6L) + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); +#else snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); +#endif int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Second Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { +#if defined(M5STACK_UNITC6L) + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); +#else snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); +#endif } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -412,9 +416,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { +#if defined(M5STACK_UNITC6L) + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); +#else snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); +#endif } else { +#if defined(M5STACK_UNITC6L) + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); +#else snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); +#endif } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { @@ -424,6 +436,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); +#if !defined(M5STACK_UNITC6L) // === Fourth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; @@ -480,6 +493,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], chUtilPercentage); +#endif } // **************************** @@ -505,8 +519,11 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, #ifdef USE_EINK barsOffset -= 12; #endif +#if defined(M5STACK_UNITC6L) + const int barX = x + 45 + barsOffset; +#else const int barX = x + 40 + barsOffset; - +#endif auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; @@ -531,7 +548,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Label display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(labelX, getTextPositions(display)[line], label); - +#if !defined(M5STACK_UNITC6L) // Bar int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); @@ -539,7 +556,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); - +#endif // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); @@ -592,10 +609,16 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, line += 1; } line += 1; + char appversionstr[35]; snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION)); char appversionstr_formatted[40]; char *lastDot = strrchr(appversionstr, '.'); +#if defined(M5STACK_UNITC6L) + if (lastDot != nullptr) { + *lastDot = '\0'; // truncate string + } +#else if (lastDot) { size_t prefixLen = lastDot - appversionstr; strncpy(appversionstr_formatted, appversionstr, prefixLen); @@ -606,10 +629,12 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); appversionstr[sizeof(appversionstr) - 1] = '\0'; } +#endif int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line], appversionstr); + display->drawString(nameX, getTextPositions(display)[line], appversionstr); +#if !defined(M5STACK_UNITC6L) if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it line += 1; char uptimeStr[32] = ""; @@ -628,6 +653,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line], uptimeStr); } +#endif } } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..ba554dbd6 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -79,7 +79,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration) "NP_865", "BR_902"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "LoRa Region"; +#else bannerOptions.message = "Set the LoRa region"; +#endif bannerOptions.durationMs = duration; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 27; @@ -260,7 +264,11 @@ void menuHandler::TZPicker() void menuHandler::clockMenu() { +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; +#else static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; +#endif enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "Clock Action"; @@ -284,8 +292,11 @@ void menuHandler::clockMenu() void menuHandler::messageResponseMenu() { enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; - +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"}; +#else static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; +#endif static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; int options = 3; @@ -299,7 +310,11 @@ void menuHandler::messageResponseMenu() optionsEnumArray[options++] = Aloud; #endif BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Message"; +#else bannerOptions.message = "Message Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -349,7 +364,11 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Send Position"; optionsEnumArray[options++] = Position; +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "New Preset"; +#else optionsArray[options] = "New Preset Msg"; +#endif optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; @@ -357,7 +376,11 @@ void menuHandler::homeBaseMenu() } BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Home"; +#else bannerOptions.message = "Home Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -396,6 +419,11 @@ void menuHandler::homeBaseMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::textMessageMenu() +{ + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); +} + void menuHandler::textMessageBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, enumEnd }; @@ -434,16 +462,22 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \ - defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT +#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ + defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif - +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "Bluetooth"; +#else optionsArray[options] = "Bluetooth Toggle"; +#endif optionsEnumArray[options++] = Bluetooth; - +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "Power"; +#else optionsArray[options] = "Reboot/Shutdown"; +#endif optionsEnumArray[options++] = PowerMenu; if (test_enabled) { @@ -452,7 +486,11 @@ void menuHandler::systemBaseMenu() } BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "System"; +#else bannerOptions.message = "System Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; @@ -485,7 +523,11 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[enumEnd] = {"Back", "New Preset"}; +#else static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; +#endif static int optionsEnumArray[enumEnd] = {Back, Preset}; int options = 2; @@ -493,13 +535,19 @@ void menuHandler::favoriteBaseMenu() optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } +#if !defined(M5STACK_UNITC6L) optionsArray[options] = "Trace Route"; optionsEnumArray[options++] = TraceRoute; +#endif optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Favorites"; +#else bannerOptions.message = "Favorites Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -554,11 +602,19 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"}; +#else static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; +#if defined(M5STACK_UNITC6L) + bannerOptions.optionsCount = 3; +#else bannerOptions.optionsCount = 5; +#endif bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -665,7 +721,11 @@ void menuHandler::BluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Bluetooth"; +#else bannerOptions.message = "Toggle Bluetooth"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { @@ -725,7 +785,7 @@ void menuHandler::BrightnessPickerMenu() #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) // For HELTEC devices, use analogWrite to control backlight analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) +#elif defined(ST7789_CS) || defined(ST7796_CS) static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); @@ -768,7 +828,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 10; bannerOptions.bannerCallback = [display](int selected) -> void { -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_b = 0; @@ -857,7 +917,11 @@ void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Reboot"; +#else bannerOptions.message = "Reboot Device?"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -877,7 +941,11 @@ void menuHandler::shutdownMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Shutdown"; +#else bannerOptions.message = "Shutdown Device?"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -894,7 +962,12 @@ void menuHandler::shutdownMenu() void menuHandler::addFavoriteMenu() { +#if defined(M5STACK_UNITC6L) + screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void { +#else screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { + +#endif LOG_WARN("Nodenum: %u", nodenum); nodeDB->set_favorite(true, nodenum); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); @@ -1045,7 +1118,7 @@ void menuHandler::screenOptionsMenu() } // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif @@ -1090,7 +1163,11 @@ void menuHandler::powerMenu() #endif BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Power"; +#else bannerOptions.message = "Reboot / Shutdown"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index b15cf237d..ed49a89fb 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -76,6 +76,7 @@ class menuHandler static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); + static void textMessageMenu(); private: static void saveUIConfig(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 117829167..6971826de 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -181,12 +181,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - +#if defined(M5STACK_UNITC6L) + const int fixedTopHeight = 24; + const int windowX = 0; + const int windowY = fixedTopHeight; + const int windowWidth = 64; + const int windowHeight = SCREEN_HEIGHT - fixedTopHeight; +#else const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; const int textWidth = SCREEN_WIDTH; +#endif bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; @@ -201,7 +208,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 graphics::drawCommonHeader(display, x, y, titleStr); const char *messageString = "No messages"; int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); +#if defined(M5STACK_UNITC6L) + display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString); +#else display->drawString(center_text, getTextPositions(display)[2], messageString); +#endif return; } @@ -209,6 +220,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); char headerStr[80]; const char *sender = "???"; +#if defined(M5STACK_UNITC6L) + if (node && node->has_user) + sender = node->user.short_name; +#else if (node && node->has_user) { if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { sender = node->user.long_name; @@ -216,6 +231,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 sender = node->user.short_name; } } +#endif uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint8_t timestampHours, timestampMinutes; int32_t daysAgo; @@ -235,10 +251,61 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 sender); } } else { +#if defined(M5STACK_UNITC6L) + snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); +#else snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), sender); +#endif } +#if defined(M5STACK_UNITC6L) + graphics::drawCommonHeader(display, x, y, titleStr); + int headerY = getTextPositions(display)[1]; + display->drawString(x, headerY, headerStr); + for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) { + display->setPixel(separatorX, fixedTopHeight - 1); + } + cachedLines.clear(); + std::string fullMsg(messageBuf); + std::string currentLine; + for (size_t i = 0; i < fullMsg.size();) { + unsigned char c = fullMsg[i]; + size_t charLen = 1; + if ((c & 0xE0) == 0xC0) + charLen = 2; + else if ((c & 0xF0) == 0xE0) + charLen = 3; + else if ((c & 0xF8) == 0xF0) + charLen = 4; + std::string nextChar = fullMsg.substr(i, charLen); + std::string testLine = currentLine + nextChar; + if (display->getStringWidth(testLine.c_str()) > windowWidth) { + cachedLines.push_back(currentLine); + currentLine = nextChar; + } else { + currentLine = testLine; + } + i += charLen; + } + if (!currentLine.empty()) + cachedLines.push_back(currentLine); + cachedHeights = calculateLineHeights(cachedLines, emotes); + int yOffset = windowY; + int linesDrawn = 0; + for (size_t i = 0; i < cachedLines.size(); ++i) { + if (linesDrawn >= 2) + break; + int lineHeight = cachedHeights[i]; + if (yOffset + lineHeight > windowY + windowHeight) + break; + drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes); + yOffset += lineHeight; + linesDrawn++; + } + screen->forceDisplay(); +#else uint32_t now = millis(); #ifndef EXCLUDE_EMOJI // === Bounce animation setup === @@ -355,6 +422,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Draw header at the end to sort out overlapping elements graphics::drawCommonHeader(display, x, y, titleStr); +#endif } std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index d8746fb69..7d6a38dd3 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -21,6 +21,10 @@ extern bool haveGlyphs(const char *str); // Global screen instance extern graphics::Screen *screen; +#if defined(M5STACK_UNITC6L) +static uint32_t lastSwitchTime = 0; +#else +#endif namespace graphics { namespace NodeListRenderer @@ -393,9 +397,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; - +#if defined(M5STACK_UNITC6L) + int columnWidth = display->getWidth(); +#else int columnWidth = display->getWidth() / 2; - +#endif display->clear(); // Draw the battery/time header @@ -408,8 +414,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int visibleNodeRows = totalRowsAvailable; +#if defined(M5STACK_UNITC6L) + int totalColumns = 1; +#else int totalColumns = 2; - +#endif int startIndex = scrollIndex * visibleNodeRows * totalColumns; if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { startIndex++; // skip own node @@ -445,12 +454,14 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } } +#if !defined(M5STACK_UNITC6L) // Draw column separator if (shownCount > 0) { const int firstNodeY = y + 3; drawColumnSeparator(display, x, firstNodeY, lastNodeY); } +#endif const int scrollStartY = y + 3; drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } @@ -468,6 +479,13 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, unsigned long now = millis(); +#if defined(M5STACK_UNITC6L) + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } +#endif // On very first call (on boot or state enter) if (lastRenderedMode == MODE_COUNT) { currentMode = MODE_LAST_HEARD; @@ -522,6 +540,14 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); +#if defined(M5STACK_UNITC6L) + display->clear(); + uint32_t now = millis(); + if (now - lastSwitchTime >= 2000) { + display->display(); + lastSwitchTime = now; + } +#endif if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { #if HAS_GPS if (screen->hasHeading()) { diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3d635e588..c2bd1ba66 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -459,6 +459,135 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay // count lines uint16_t boxWidth = hPadding * 2 + maxWidth; +#if defined(M5STACK_UNITC6L) + if (needs_bell) { + if (isHighResolution && boxWidth <= 150) + boxWidth += 26; + if (!isHighResolution && boxWidth <= 100) + boxWidth += 20; + } + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) + boxHeight += (isHighResolution ? 4 : 3); + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) + boxWidth += (isHighResolution ? 4 : 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + + if (visibleTotalLines == 1) { + boxTop += 25; + } + if (alertBannerOptions < 3) { + int missingLines = 3 - alertBannerOptions; + int moveUp = missingLines * (effectiveLineHeight / 2); + boxTop -= moveUp; + if (boxTop < 0) + boxTop = 0; + } + + // === Draw Box === + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + int16_t lineY = boxTop + vPadding; + int swingRange = 8; + static int swingOffset = 0; + static bool swingRight = true; + static unsigned long lastSwingTime = 0; + unsigned long now = millis(); + int swingSpeedMs = 10 / (swingRange * 2); + if (now - lastSwingTime >= (unsigned long)swingSpeedMs) { + lastSwingTime = now; + if (swingRight) { + swingOffset++; + if (swingOffset >= swingRange) + swingRight = false; + } else { + swingOffset--; + if (swingOffset <= 0) + swingRight = true; + } + } + for (int i = 0; i < lineCount; i++) { + bool isTitle = (i == 0); + int globalOptionIndex = (i - 1) + firstOptionToShow; + bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected); + + uint16_t visibleWidth = 64 - hPadding * 2; + if (totalLines > visibleTotalLines) + visibleWidth -= 6; + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; + + if (isTitle) { + if (visibleTotalLines == 1) { + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); + display->setColor(WHITE); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); + } else { + display->setColor(WHITE); + display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); + display->setColor(BLACK); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); + display->setColor(WHITE); + if (needs_bell) { + int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert); + display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert); + } + } + lineY = boxTop + effectiveLineHeight + 1; + } else if (isSelectedOption) { + display->setColor(WHITE); + display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); + display->setColor(BLACK); + if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) { + int textX = boxLeft + hPadding + swingOffset; + display->drawString(textX, lineY - 1, lineBuffer); + } else { + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer); + } + display->setColor(WHITE); + lineY += effectiveLineHeight; + } else { + display->setColor(BLACK); + display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); + display->setColor(WHITE); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer); + lineY += effectiveLineHeight; + } + } + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + } +#else if (needs_bell) { if (isHighResolution && boxWidth <= 150) boxWidth += 26; @@ -547,6 +676,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } +#endif } /// Draw the last text message we received diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 71d92616f..e00b19b2f 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -20,7 +20,7 @@ // External variables extern graphics::Screen *screen; - +static uint32_t lastSwitchTime = 0; namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; @@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) if (isHighResolution) { @@ -218,7 +218,6 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes // ********************** void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (favoritedNodes.empty()) return; @@ -230,8 +229,15 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; - + uint32_t now = millis(); display->clear(); +#if defined(M5STACK_UNITC6L) + if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 + { + display->display(); + lastSwitchTime = now; + } +#endif currentFavoriteNodeNum = node->num; // === Create the shortName and title string === const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; @@ -250,9 +256,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st // List of available macro Y positions in order, from top to bottom. int line = 1; // which slot to use next std::string usernameStr; - // === 1. Long Name (always try to show first) === +#if defined(M5STACK_UNITC6L) + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; +#else const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; +#endif + if (username) { usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case // Print node's long name (e.g. "Backpack Node") @@ -307,7 +317,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (seenStr[0] && line < 5) { display->drawString(x, getTextPositions(display)[line++], seenStr); } - +#if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { @@ -479,6 +489,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st } // else show nothing } +#endif } // **************************** @@ -492,7 +503,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int line = 1; // === Header === +#if defined(M5STACK_UNITC6L) + graphics::drawCommonHeader(display, x, y, "Home"); +#else graphics::drawCommonHeader(display, x, y, ""); +#endif // === Content below header === @@ -507,20 +522,25 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta config.display.heading_bold = false; // Display Region and Channel Utilization +#if defined(M5STACK_UNITC6L) + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); +#else drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - +#endif char uptimeStr[32] = ""; uint32_t uptime = millis() / 1000; uint32_t days = uptime / 86400; uint32_t hours = (uptime % 86400) / 3600; uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" +#if !defined(M5STACK_UNITC6L) if (days) snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); else if (hours) snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); else snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); +#endif display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); // === Second Row: Satellites and Voltage === @@ -549,6 +569,21 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } #endif +#if defined(M5STACK_UNITC6L) + line += 1; + + // === Node Identity === + int textWidth = 0; + int nameX = 0; + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); +#else if (powerStatus->getHasBattery()) { char batStr[20]; int batV = powerStatus->getBatteryVoltageMv() / 1000; @@ -674,6 +709,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], shortnameble); } +#endif } // Start Functions to write date/time to the screen @@ -832,6 +868,28 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED // needs to be drawn relative to x and y // draw centered icon left to right and centered above the one line of app text +#if defined(M5STACK_UNITC6L) + display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) { + int msgWidth = display->getStringWidth(upperMsg); + int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; + int msgY = y; + display->drawString(msgX, msgY, upperMsg); + } + // Draw version and short name in bottom middle + char buf[25]; + snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +#else display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, icon_bits); @@ -840,7 +898,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED const char *title = "meshtastic.org"; display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); - // Draw region in upper left if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); @@ -855,6 +912,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +#endif } // **************************** @@ -930,15 +988,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); char fullLine[40]; snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); +#if !defined(M5STACK_UNITC6L) display->drawString(0, getTextPositions(display)[line++], fullLine); +#endif // === Third Row: Latitude === char latStr[32]; +#if defined(M5STACK_UNITC6L) + snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++] + 2, latStr); +#else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); +#endif // === Fourth Row: Longitude === char lonStr[32]; +#if defined(M5STACK_UNITC6L) + snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); +#else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); @@ -950,8 +1019,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); +#endif } - +#if !defined(M5STACK_UNITC6L) // === Draw Compass if heading is valid === if (validHeading) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- @@ -1034,6 +1104,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } } #endif +#endif } #ifdef USERPREFS_OEM_TEXT @@ -1190,7 +1261,6 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(WHITE); } } - // Knock the corners off the square display->setColor(BLACK); display->drawRect(rectX, y - 2, 1, 1); diff --git a/src/graphics/images.h b/src/graphics/images.h index beef3a1b2..4a58edb3b 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; @@ -286,6 +287,9 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101 #define analog_icon_clock_height 8 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; - +#ifdef M5STACK_UNITC6L +#include "img/icon_small.xbm" +#else #include "img/icon.xbm" +#endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/graphics/img/icon_small.xbm b/src/graphics/img/icon_small.xbm new file mode 100644 index 000000000..e320a1fea --- /dev/null +++ b/src/graphics/img/icon_small.xbm @@ -0,0 +1,30 @@ +#ifndef USERPREFS_HAS_SPLASH +#define icon_width 50 +#define icon_height 20 +static uint8_t icon_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x80, + 0x07, 0xc0, 0x07, 0x00, 0x00, 0x00, 0xc0, 0x0f, + 0xc0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xe0, + 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xf0, 0x1f, + 0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x3f, 0x00, + 0x00, 0x00, 0xf8, 0x03, 0xf8, 0x7f, 0x00, 0x00, + 0x00, 0xf8, 0x01, 0xfc, 0x7e, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0xfe, + 0x00, 0x7e, 0xf8, 0x00, 0x00, 0x00, 0x7e, 0x00, + 0x3f, 0xf8, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x1f, + 0xf0, 0x01, 0x00, 0x00, 0x1f, 0x80, 0x1f, 0xe0, + 0x03, 0x00, 0x80, 0x1f, 0xc0, 0x0f, 0xe0, 0x03, + 0x00, 0x80, 0x0f, 0xc0, 0x07, 0xc0, 0x07, 0x00, + 0xc0, 0x0f, 0xe0, 0x07, 0x80, 0x0f, 0x00, 0xe0, + 0x07, 0xf0, 0x03, 0x80, 0x1f, 0x00, 0xe0, 0x03, + 0xf8, 0x03, 0x00, 0x1f, 0x00, 0xf0, 0x03, 0xf8, + 0x01, 0x00, 0x3f, 0x00, 0xf8, 0x01, 0xfc, 0x00, + 0x00, 0x7e, 0x00, 0xfc, 0x00, 0xfe, 0x00, 0x00, + 0x7e, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x00, 0xfc, + 0x00, 0x7e, 0x00, 0x3f, 0x00, 0x00, 0xf8, 0x00, + 0x7e, 0x00, 0x3e, 0x00, 0x00, 0xf8, 0x00, 0x38, + 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; +#endif \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp new file mode 100644 index 000000000..e83588905 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -0,0 +1,68 @@ +#include "./ZJY122250_0213BAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY122250_0213BAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from +// the controller IC's OTP memory, when the update procedure begins. +void ZJY122250_0213BAAMFGN::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY122250_0213BAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } +} + +// Once the refresh operation has been started, +// begin periodically polling the display to check for completion, using the normal Meshtastic threading code +// Only used when refresh is "async" +void ZJY122250_0213BAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h new file mode 100644 index 000000000..82c4ec107 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - ZJY122250_0213BAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): FPC-A002 + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY122250_0213BAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index 7e1accafd..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,12 +7,7 @@ using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active - -#ifdef SEEED_WIO_TRACKER_L1 -static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL; -#else static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; -#endif static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp new file mode 100644 index 000000000..7d638dd71 --- /dev/null +++ b/src/input/RotaryEncoderImpl.cpp @@ -0,0 +1,73 @@ +#ifdef T_LORA_PAGER + +#include "RotaryEncoderImpl.h" +#include "InputBroker.h" +#include "RotaryEncoder.h" + +#define ORIGIN_NAME "RotaryEncoder" + +RotaryEncoderImpl *rotaryEncoderImpl; + +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) +{ + rotary = nullptr; +} + +bool RotaryEncoderImpl::init() +{ + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + disable(); + return false; + } + + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + rotary->resetButton(); + + inputBroker->registerSource(this); + + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, + eventPressed); + return true; +} + +int32_t RotaryEncoderImpl::runOnce() +{ + InputEvent e{originName, INPUT_BROKER_NONE, 0, 0, 0}; + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + } + } else { + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + break; + default: + break; + } + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + + return 10; +} + +#endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h new file mode 100644 index 000000000..ae2a7c6fd --- /dev/null +++ b/src/input/RotaryEncoderImpl.h @@ -0,0 +1,28 @@ +#pragma once + +// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library) + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +class RotaryEncoder; + +class RotaryEncoderImpl : public Observable, public concurrency::OSThread +{ + public: + RotaryEncoderImpl(); + bool init(void); + + protected: + virtual int32_t runOnce() override; + + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; + + RotaryEncoder *rotary; + const char *originName; +}; + +extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 0557bc180..88b07a389 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -18,14 +18,23 @@ void RotaryEncoderInterruptBase::init( this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinA, INPUT_PULLUP); - pinMode(this->_pinB, INPUT_PULLUP); + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - // attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinA, onIntA, CHANGE); - attachInterrupt(this->_pinB, onIntB, CHANGE); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); + } this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelB = digitalRead(this->_pinB); diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp new file mode 100644 index 000000000..b3f62a36c --- /dev/null +++ b/src/input/TLoraPagerKeyboard.cpp @@ -0,0 +1,230 @@ +#if defined(T_LORA_PAGER) + +#include "TLoraPagerKeyboard.h" +#include "main.h" + +#ifndef LEDC_BACKLIGHT_CHANNEL +#define LEDC_BACKLIGHT_CHANNEL 4 +#endif + +#ifndef LEDC_BACKLIGHT_BIT_WIDTH +#define LEDC_BACKLIGHT_BIT_WIDTH 8 +#endif + +#ifndef LEDC_BACKLIGHT_FREQ +#define LEDC_BACKLIGHT_FREQ 1000 // Hz +#endif + +#define _TCA8418_COLS 10 +#define _TCA8418_ROWS 4 +#define _TCA8418_NUM_KEYS 31 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1 +constexpr uint8_t modifierRightShift = 0b0001; +constexpr uint8_t modifierSymKey = 21 - 1; +constexpr uint8_t modifierSym = 0b0010; + +// Num chars per key, Modulus for rotating through characters +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, + {'w', 'W', '2'}, + {'e', 'E', '3'}, + {'r', 'R', '4'}, + {'t', 'T', '5'}, + {'y', 'Y', '6'}, + {'u', 'U', '7'}, + {'i', 'I', '8'}, + {'o', 'O', '9'}, + {'p', 'P', '0'}, + {'a', 'A', '*'}, + {'s', 'S', '/'}, + {'d', 'D', '+'}, + {'f', 'F', '-'}, + {'g', 'G', '='}, + {'h', 'H', ':'}, + {'j', 'J', '\''}, + {'k', 'K', '"'}, + {'l', 'L', '@'}, + {Key::SELECT, 0x00, Key::TAB}, + {0x00, 0x00, 0x00}, + {'z', 'Z', '_'}, + {'x', 'X', '$'}, + {'c', 'C', ';'}, + {'v', 'V', '?'}, + {'b', 'B', '!'}, + {'n', 'N', ','}, + {'m', 'M', '.'}, + {0x00, 0x00, 0x00}, + {Key::BSP, 0x00, Key::ESC}, + {' ', 0x00, Key::BL_TOGGLE}}; + +TLoraPagerKeyboard::TLoraPagerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); +#else + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); +#endif + reset(); +} + +void TLoraPagerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); +} + +// handle multi-key presses (shift and alt) +void TLoraPagerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void TLoraPagerKeyboard::setBacklight(bool on) +{ + toggleBacklight(!on); +} + +void TLoraPagerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void TLoraPagerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } + + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void TLoraPagerKeyboard::hapticFeedback() +{ + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); +} + +// toggle brightness of the backlight in three steps +void TLoraPagerKeyboard::toggleBacklight(bool off) +{ + static uint32_t brightness = 0; + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; + } + } + LOG_DEBUG("Toggle backlight: %d", brightness); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcWrite(KB_BL_PIN, brightness); +#else + ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness); +#endif +} + +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } +} + +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierSymKey); +} + +#endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index d31b05978..4dabbac64 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -4,9 +4,26 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase { public: TLoraPagerKeyboard(); - void setBacklight(bool on) override{}; + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} protected: - void pressed(uint8_t key) override{}; - void released(void) override{}; + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); + + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index c66eb13d0..26b281aaf 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -15,14 +15,23 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, RISING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, RISING); + } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index fcbdd0a3f..cb03eb4ff 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -12,8 +12,12 @@ void CardKbI2cImpl::init() #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS}; + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; +#if defined(T_LORA_PAGER) + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); +#else uint8_t i2caddr_asize = 5; +#endif auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 diff --git a/src/input/i2cButton.cpp b/src/input/i2cButton.cpp new file mode 100644 index 000000000..d874146cd --- /dev/null +++ b/src/input/i2cButton.cpp @@ -0,0 +1,95 @@ +#include "i2cButton.h" +#include "meshUtils.h" + +#include "configuration.h" +#if defined(M5STACK_UNITC6L) + +#include "MeshService.h" +#include "RadioLibInterface.h" +#include "buzz.h" +#include "input/InputBroker.h" +#include "main.h" +#include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" +#include "power.h" +#include "sleep.h" +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +i2cButtonThread *i2cButton; + +using namespace concurrency; + +extern void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value); + +extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value); + +#define PI4IO_M_ADDR 0x43 +#define getbit(x, y) ((x) >> (y)&0x01) +#define PI4IO_REG_IRQ_STA 0x13 +#define PI4IO_REG_IN_STA 0x0F +#define PI4IO_REG_CHIP_RESET 0x01 + +i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) +{ + _originName = name; + if (inputBroker) + inputBroker->registerSource(this); +} + +int32_t i2cButtonThread::runOnce() +{ + static bool btn1_pressed = false; + static uint32_t press_start_time = 0; + const uint32_t LONG_PRESS_TIME = 1000; + static bool long_press_triggered = false; + + uint8_t in_data; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); + if (getbit(in_data, 0)) { + uint8_t input_state; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); + + if (!getbit(input_state, 0)) { + if (!btn1_pressed) { + btn1_pressed = true; + press_start_time = millis(); + long_press_triggered = false; + } + } else { + if (btn1_pressed) { + btn1_pressed = false; + uint32_t press_duration = millis() - press_start_time; + if (long_press_triggered) { + long_press_triggered = false; + return 50; + } + + if (press_duration < LONG_PRESS_TIME) { + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_USER_PRESS; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + } + } + } + + if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { + long_press_triggered = true; + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_SELECT; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + return 50; +} +#endif \ No newline at end of file diff --git a/src/input/i2cButton.h b/src/input/i2cButton.h new file mode 100644 index 000000000..1ad908606 --- /dev/null +++ b/src/input/i2cButton.h @@ -0,0 +1,18 @@ +#pragma once + +#include "InputBroker.h" +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#if defined(M5STACK_UNITC6L) + +class i2cButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + explicit i2cButtonThread(const char *name); + int32_t runOnce() override; +}; + +extern i2cButtonThread *i2cButton; +#endif diff --git a/src/main.cpp b/src/main.cpp index 0260cbc07..d7e866a2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,8 +135,9 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif -#ifdef USE_PCA9557 -PCA9557 IOEXP; +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +ExtensionIOXL9555 io; #endif #if HAS_TFT @@ -201,7 +202,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) Adafruit_DRV2605 drv; #endif @@ -359,8 +360,31 @@ void setup() 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); + // 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, HIGH); + 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); #endif - concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0); @@ -394,7 +418,7 @@ void setup() struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityDevice, &tv); #endif powerMonInit(); @@ -519,6 +543,12 @@ void setup() #endif #endif +#if defined(M5STACK_UNITC6L) + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, 1); + c6l_init(); +#endif + #ifdef PIN_LCD_RESET // FIXME - move this someplace better, LCD is at address 0x3F pinMode(PIN_LCD_RESET, OUTPUT); @@ -716,6 +746,7 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::TSL2561, meshtastic_TelemetrySensorType_TSL2561); i2cScanner.reset(); #endif @@ -805,7 +836,7 @@ void setup() #endif #endif -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -851,7 +882,8 @@ void setup() if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_SPISSD1306) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && @@ -1114,7 +1146,8 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_SPISSD1306) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) @@ -1500,7 +1533,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif -#if NO_EXT_GPIO +#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts diff --git a/src/main.h b/src/main.h index 3568daad2..ef1f241ef 100644 --- a/src/main.h +++ b/src/main.h @@ -41,7 +41,7 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #include extern Adafruit_DRV2605 drv; #endif diff --git a/src/memGet.cpp b/src/memGet.cpp index e8cd177dd..14e614014 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -88,4 +88,16 @@ uint32_t MemGet::getPsramSize() #else return 0; #endif +} + +void displayPercentHeapFree() +{ + uint32_t freeHeap = memGet.getFreeHeap(); + uint32_t totalHeap = memGet.getHeapSize(); + if (totalHeap == 0 || totalHeap == UINT32_MAX) { + LOG_INFO("Heap size unavailable"); + return; + } + int percent = (int)((freeHeap * 100) / totalHeap); + LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); } \ No newline at end of file diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 70e4127d8..4ef41ddfb 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -368,7 +368,7 @@ const char *Channels::getName(size_t chIndex) // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // the app effed up and forgot to set channelSettings.name if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); } else { channelName = "Custom"; } @@ -382,7 +382,8 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex) const auto &ch = getByIndex(chIndex); if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { const char *name = getName(chIndex); - const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); // Check if the name is the default derived from the modem preset if (strcmp(name, presetName) == 0) return true; diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index c4af3c4ac..eb5ac5109 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -6,6 +6,7 @@ #include #include "PointerQueue.h" +#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP template class Allocator { @@ -14,13 +15,14 @@ template class Allocator Allocator() : deleter([this](T *p) { this->release(p); }) {} virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Note: this method is safe to call from regular OR ISR code T *allocZeroed() { T *p = allocZeroed(0); - - assert(p); // FIXME panic instead + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } return p; } @@ -39,10 +41,12 @@ template class Allocator T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { T *p = alloc(maxWait); - assert(p); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; + } - if (p) - *p = src; + *p = src; return p; } @@ -83,7 +87,11 @@ template class MemoryDynamic : public Allocator /// Return a buffer for use by others virtual void release(T *p) override { - assert(p); + if (p == nullptr) + return; + + LOG_HEAP("Freeing 0x%x", p); + free(p); } @@ -96,3 +104,58 @@ template class MemoryDynamic : public Allocator return p; } }; + +/** + * A static memory pool that uses a fixed buffer instead of heap allocation + */ +template class MemoryPool : public Allocator +{ + private: + T pool[MaxSize]; + bool used[MaxSize]; + + public: + MemoryPool() : pool{}, used{} + { + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) + } + + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; + } + + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } + } + + protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override + { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } +}; diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 409c52179..c5748a560 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -85,11 +85,8 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e return r; } -void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char *specificModule) +void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) { - if (specificModule) { - LOG_DEBUG("Calling specific module: %s", specificModule); - } // LOG_DEBUG("In call modules"); bool moduleFound = false; @@ -107,11 +104,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; - // If specificModule is provided, only call that specific module - if (specificModule && (!pi.name || strcmp(pi.name, specificModule) != 0)) { - continue; - } - pi.currentRequest = ∓ /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index bf735439f..eda3f8881 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -73,7 +73,7 @@ class MeshModule /** For use only by MeshService */ - static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO, const char *specificModule = nullptr); + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static std::vector GetMeshModulesWithUIFrames(int startIndex); static void observeUIEvents(Observer *observer); diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 2cc4197c1..96782cda5 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -46,11 +46,14 @@ the new node can build its node db) MeshService *service; -static MemoryDynamic staticMqttClientProxyMessagePool; +#define MAX_MQTT_PROXY_MESSAGES 16 +static MemoryPool staticMqttClientProxyMessagePool; -static MemoryDynamic staticQueueStatusPool; +#define MAX_QUEUE_STATUS 4 +static MemoryPool staticQueueStatusPool; -static MemoryDynamic staticClientNotificationPool; +#define MAX_CLIENT_NOTIFICATIONS 4 +static MemoryPool staticClientNotificationPool; Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; @@ -61,8 +64,10 @@ Allocator &queueStatusPool = staticQueueStatusPool; #include "Router.h" MeshService::MeshService() - : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE), - toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2) +#ifdef ARCH_PORTDUINO + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), + toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE) +#endif { lastQueueStatus = {0, 0, 16, 0}; } @@ -191,8 +196,10 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) // (so we update our nodedb for the local node) // Send the packet into the mesh - - sendToMesh(packetPool.allocCopy(p), RX_SRC_USER); + DEBUG_HEAP_BEFORE; + auto a = packetPool.allocCopy(p); + DEBUG_HEAP_AFTER("MeshService::handleToRadio", a); + sendToMesh(a, RX_SRC_USER); bool loopback = false; // if true send any packet the phone sends back itself (for testing) if (loopback) { @@ -248,7 +255,11 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh } if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent - sendToPhone(packetPool.allocCopy(*p)); + DEBUG_HEAP_BEFORE; + auto a = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("MeshService::sendToMesh", a); + + sendToPhone(a); } // Router may ask us to release the packet if it wasn't sent diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index f7d79366e..5d074368f 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -9,7 +9,12 @@ #include "MeshRadio.h" #include "MeshTypes.h" #include "Observer.h" +#ifdef ARCH_PORTDUINO #include "PointerQueue.h" +#else +#include "StaticPointerQueue.h" +#endif +#include "mesh-pb-constants.h" #if defined(ARCH_PORTDUINO) #include "../platform/portduino/SimRadio.h" #endif @@ -37,16 +42,32 @@ class MeshService /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure /// we never hang because android hasn't been there in a while /// FIXME - save this to flash on deep sleep +#ifdef ARCH_PORTDUINO PointerQueue toPhoneQueue; +#else + StaticPointerQueue toPhoneQueue; +#endif // keep list of QueueStatus packets to be send to the phone +#ifdef ARCH_PORTDUINO PointerQueue toPhoneQueueStatusQueue; +#else + StaticPointerQueue toPhoneQueueStatusQueue; +#endif // keep list of MqttClientProxyMessages to be send to the client for delivery +#ifdef ARCH_PORTDUINO PointerQueue toPhoneMqttProxyQueue; +#else + StaticPointerQueue toPhoneMqttProxyQueue; +#endif // keep list of ClientNotifications to be send to the client (phone) +#ifdef ARCH_PORTDUINO PointerQueue toPhoneClientNotificationQueue; +#else + StaticPointerQueue toPhoneClientNotificationQueue; +#endif // This holds the last QueueStatus send meshtastic_QueueStatus lastQueueStatus; diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 860250f75..db3d62038 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -34,8 +34,11 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) bool weWereNextHop = false; if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record printPacket("Ignore dupe incoming msg", p); - rxDupe++; - stopRetransmission(p->from, p->id); + + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + rxDupe++; + stopRetransmission(p->from, p->id); + } // If it was a fallback to flooding, try to relay again if (wasFallback) { @@ -71,10 +74,11 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast if (p->from != 0) { meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); if (origTx) { - // Either relayer of ACK was also a relayer of the packet, or we were the relayer and the ACK came directly from - // the destination + // Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly + // from the destination if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) || - (wasRelayer(ourRelayID, p->decoded.request_id, p->to) && p->hop_start != 0 && p->hop_start == p->hop_limit)) { + (p->hop_start != 0 && p->hop_start == p->hop_limit && + wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node); origTx->next_hop = p->relay_node; @@ -171,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); - // now free the pooled copy for retransmission too - packetPool.release(p); } } + + // Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't + // get scheduled again. (This is the core of stopRetransmission.) auto numErased = pending.erase(key); assert(numErased == 1); + + // When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call + // to startRetransmission. + packetPool.release(p); + return true; } else return false; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 18014eb02..6473722d7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); @@ -830,6 +830,15 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; +#endif +#ifdef T_LORA_PAGER + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT @@ -1702,10 +1711,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { - // if (mp.from == getNodeNum()) { - // LOG_DEBUG("Ignore update from self"); - // return; - // } + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; + } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 3902c1057..735386d79 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -294,7 +294,7 @@ void PacketHistory::insert(const PacketRecord &r) /* Check if a certain node was a relayer of a packet in the history given an ID and sender * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) { if (!initOk()) { LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); @@ -322,27 +322,42 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); #endif - return wasRelayer(relayer, *found); + return wasRelayer(relayer, *found, wasSole); } /* Check if a certain node was a relayer of a packet in the history given iterator * @return true if node was indeed a relayer, false if not */ -bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r) +bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) { - for (uint8_t i = 0; i < NUM_RELAYERS; i++) { + bool found = false; + bool other_present = false; + + for (uint8_t i = 0; i < NUM_RELAYERS; ++i) { if (r.relayed_by[i] == relayer) { -#if VERBOSE_PACKET_HISTORY - LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id, - r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); -#endif - return true; + found = true; + } else if (r.relayed_by[i] != 0) { + other_present = true; } } + + if (wasSole) { + *wasSole = (found && !other_present); + } + #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); #endif - return false; + + return found; +} + +// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender +bool PacketHistory::wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) +{ + bool wasSole = false; + wasRelayer(relayer, id, sender, &wasSole); + return wasSole; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 9f14a4cf0..4b53c8f6a 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -34,8 +34,9 @@ class PacketHistory void insert(const PacketRecord &r); // Insert or replace a packet record in the history /* Check if a certain node was a relayer of a packet in the history given iterator + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const PacketRecord &r); + bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); PacketHistory(const PacketHistory &); // non construction-copyable PacketHistory &operator=(const PacketHistory &); // non copyable @@ -54,8 +55,12 @@ class PacketHistory bool *weWereNextHop = nullptr); /* Check if a certain node was a relayer of a packet in the history given an ID and sender + * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet * @return true if node was indeed a relayer, false if not */ - bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); + bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); + + // Check if a certain node was the *only* relayer of a packet in the history given an ID and sender + bool wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index a3a8a2087..9fb1b589f 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -100,6 +100,7 @@ void PhoneAPI::close() config_nonce = 0; config_state = 0; pauseBluetoothLogging = false; + heartbeatReceived = false; } } diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c210d5d48..a5c293868 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -586,7 +586,8 @@ void RadioInterface::applyModemConfig() // Check if we use the default frequency slot RadioInterface::uses_default_frequency_slot = - channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels; + channel_num == + hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; // Old frequency selection formula // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 6e5c6231b..6d098b669 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -2,6 +2,7 @@ #include "Default.h" #include "MeshTypes.h" #include "configuration.h" +#include "memGet.h" #include "mesh-pb-constants.h" #include "modules/NodeInfoModule.h" #include "modules/RoutingModule.h" @@ -21,8 +22,10 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) 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); + startRetransmission(copy, NUM_RELIABLE_RETX); } @@ -58,7 +61,10 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) // marked as wantAck sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); - stopRetransmission(key); + // Only stop retransmissions if the rebroadcast came via LoRa + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + stopRetransmission(key); + } } else { LOG_DEBUG("Didn't find pending packet"); } diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index cceacfe9e..6c5d08a93 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -5,6 +5,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "RTC.h" + #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" @@ -27,14 +28,24 @@ // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment + +#ifdef ARCH_PORTDUINO +// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -// static MemoryPool staticPool(MAX_PACKETS); -static MemoryDynamic staticPool; +static MemoryDynamic dynamicPool; +Allocator &packetPool = dynamicPool; +#else +// Embedded targets use static memory pools with compile-time constants +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +static MemoryPool staticPool; Allocator &packetPool = staticPool; +#endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); @@ -275,7 +286,10 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) // If the packet is not yet encrypted, do so now if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it + + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::send", p_decoded); auto encodeResult = perhapsEncode(p); if (encodeResult != meshtastic_Routing_Error_NONE) { @@ -529,8 +543,9 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) #endif // Don't use PKC with Ham mode !owner.is_licensed && - // Don't use PKC if it's not explicitly requested and a non-primary channel is requested - !(p->pki_encrypted != true && p->channel > 0) && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && // Check for a known public key for the destination @@ -561,7 +576,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -577,7 +592,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -606,8 +621,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) bool skipHandle = false; // Also, we should set the time from the ISR and it should have msec level resolution p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone + // Store a copy of encrypted packet for MQTT + DEBUG_HEAP_BEFORE; meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); + DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand auto decodedState = perhapsDecode(p); @@ -655,7 +673,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // call modules here // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle && p->from != nodeDB->getNodeNum()) { + if (!skipHandle) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT @@ -669,8 +687,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) !isFromUs(p) && mqtt) mqtt->onSend(*p_encrypted, *p, p->channel); #endif - } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src, ROUTING_MODULE); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index d7bc37466..f6e4b3512 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -1,13 +1,13 @@ -#include "STM32WLE5JCInterface.h" #include "configuration.h" + +#ifdef ARCH_STM32WL +#include "STM32WLE5JCInterface.h" #include "error.h" #ifndef STM32WLx_MAX_POWER #define STM32WLx_MAX_POWER 22 #endif -#ifdef ARCH_STM32WL - STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) : SX126xInterface(hal, cs, irq, rst, busy) diff --git a/src/mesh/STM32WLE5JCInterface.h b/src/mesh/STM32WLE5JCInterface.h index 0c8140290..ee935375e 100644 --- a/src/mesh/STM32WLE5JCInterface.h +++ b/src/mesh/STM32WLE5JCInterface.h @@ -1,8 +1,8 @@ #pragma once -#include "SX126xInterface.h" - #ifdef ARCH_STM32WL +#include "SX126xInterface.h" +#include "rfswitch.h" /** * Our adapter for STM32WLE5JC radios @@ -16,13 +16,4 @@ class STM32WLE5JCInterface : public SX126xInterface virtual bool init() override; }; -/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/ - * Wio-E5 module ONLY transmits through RFO_HP - * Receive: PA4=1, PA5=0 - * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */ -static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; - -static const Module::RfSwitchMode_t rfswitch_table[4] = { - {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; - #endif // ARCH_STM32WL \ No newline at end of file diff --git a/src/mesh/StaticPointerQueue.h b/src/mesh/StaticPointerQueue.h new file mode 100644 index 000000000..398ee450c --- /dev/null +++ b/src/mesh/StaticPointerQueue.h @@ -0,0 +1,77 @@ +#pragma once + +#include "concurrency/OSThread.h" +#include "freertosinc.h" +#include + +/** + * A static circular buffer queue for pointers. + * This provides the same interface as PointerQueue but uses a statically allocated + * buffer instead of dynamic allocation. + */ +template class StaticPointerQueue +{ + static_assert(MaxElements > 0, "MaxElements must be greater than 0"); + + T *buffer[MaxElements]; + int head = 0; + int tail = 0; + int count = 0; + concurrency::OSThread *reader = nullptr; + + public: + StaticPointerQueue() + { + // Initialize all buffer elements to nullptr to silence warnings and ensure clean state + for (int i = 0; i < MaxElements; i++) { + buffer[i] = nullptr; + } + } + + int numFree() const { return MaxElements - count; } + + bool isEmpty() const { return count == 0; } + + int numUsed() const { return count; } + + bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY) + { + if (count >= MaxElements) { + return false; // Queue is full + } + + if (reader) { + reader->setInterval(0); + concurrency::mainDelay.interrupt(); + } + + buffer[tail] = x; + tail = (tail + 1) % MaxElements; + count++; + return true; + } + + bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY) + { + if (count == 0) { + return false; // Queue is empty + } + + *p = buffer[head]; + head = (head + 1) % MaxElements; + count--; + return true; + } + + // returns a ptr or null if the queue was empty + T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) + { + T *p; + return dequeue(&p, maxWait) ? p : nullptr; + } + + void setReader(concurrency::OSThread *t) { reader = t; } + + // For compatibility with PointerQueue interface + int getMaxLen() const { return MaxElements; } +}; diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 4a42e5197..20026767e 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -16,6 +16,95 @@ int32_t StreamAPI::runOncePart() return result; } +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) +{ + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; +} + +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} + +/** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ +void StreamAPI::writeStream() +{ + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } +} + +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } + } + return 0; +} + /** * Read any rx chars from the link and call handleToRadio */ @@ -76,21 +165,6 @@ int32_t StreamAPI::readStream() } } -/** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ -void StreamAPI::writeStream() -{ - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } -} - /** * Send the current txBuffer over our stream */ diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 6e0364bc1..547dd0175 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -50,12 +50,15 @@ class StreamAPI : public PhoneAPI * phone. */ virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf,uint16_t bufLen); private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); + int32_t readStream(char *buf,uint16_t bufLen); + int32_t handleRecStream(char *buf,uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 67d461611..59e55db3f 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -64,7 +64,12 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11 + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, + /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + Technical Details: Used for stronger attic/roof nodes to distribute messages more widely + from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes + where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 } meshtastic_Config_DeviceConfig_Role; /* Defines the device's behavior for how messages are rebroadcast */ @@ -646,8 +651,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT -#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE -#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1)) +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 8313438f8..8f693e570 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -66,6 +66,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_UKRAINIAN = 16, /* Bulgarian */ meshtastic_Language_BULGARIAN = 17, + /* Czech */ + meshtastic_Language_CZECH = 18, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index f47091384..9b6330596 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,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 2271 +#define meshtastic_BackupPreferences_size 2273 #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 ca8dcd5fb..da224fb94 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -188,7 +188,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size #define meshtastic_LocalConfig_size 747 -#define meshtastic_LocalModuleConfig_size 669 +#define meshtastic_LocalModuleConfig_size 671 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index ce3722aa7..294f0beac 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -259,8 +259,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_DECK_PRO = 102, /* Lilygo TLora Pager */ meshtastic_HardwareModel_T_LORA_PAGER = 103, - /* GAT562 Mesh Trial Tracker */ - meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104, + /* M5Stack Reserved */ + meshtastic_HardwareModel_M5STACK_RESERVED = 104, /* 0x68 */ /* RAKwireless WisMesh Tag */ meshtastic_HardwareModel_WISMESH_TAG = 105, /* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */ @@ -272,6 +272,10 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_HELTEC_MESH_SOLAR = 108, /* Lilygo T-Echo Lite */ meshtastic_HardwareModel_T_ECHO_LITE = 109, + /* New Heltec LoRA32 with ESP32-S3 CPU */ + meshtastic_HardwareModel_HELTEC_V4 = 110, + /* M5Stack C6L */ + meshtastic_HardwareModel_M5STACK_C6L = 111, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index b27f5f515..16c4c230c 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -317,6 +317,9 @@ typedef struct _meshtastic_ModuleConfig_RangeTestConfig { /* Bool value indicating that this node should save a RangeTest.csv file. ESP32 Only */ bool save; + /* Bool indicating that the node should cleanup / destroy it's RangeTest.csv file. + ESP32 Only */ + bool clear_on_reboot; } meshtastic_ModuleConfig_RangeTestConfig; /* Configuration for both device and environment metrics */ @@ -519,7 +522,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} @@ -535,7 +538,7 @@ extern "C" { #define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} -#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0} +#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} @@ -610,6 +613,7 @@ extern "C" { #define meshtastic_ModuleConfig_RangeTestConfig_enabled_tag 1 #define meshtastic_ModuleConfig_RangeTestConfig_sender_tag 2 #define meshtastic_ModuleConfig_RangeTestConfig_save_tag 3 +#define meshtastic_ModuleConfig_RangeTestConfig_clear_on_reboot_tag 4 #define meshtastic_ModuleConfig_TelemetryConfig_device_update_interval_tag 1 #define meshtastic_ModuleConfig_TelemetryConfig_environment_update_interval_tag 2 #define meshtastic_ModuleConfig_TelemetryConfig_environment_measurement_enabled_tag 3 @@ -803,7 +807,8 @@ X(a, STATIC, SINGULAR, BOOL, is_server, 6) #define meshtastic_ModuleConfig_RangeTestConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, UINT32, sender, 2) \ -X(a, STATIC, SINGULAR, BOOL, save, 3) +X(a, STATIC, SINGULAR, BOOL, save, 3) \ +X(a, STATIC, SINGULAR, BOOL, clear_on_reboot, 4) #define meshtastic_ModuleConfig_RangeTestConfig_CALLBACK NULL #define meshtastic_ModuleConfig_RangeTestConfig_DEFAULT NULL @@ -901,7 +906,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_MapReportSettings_size 14 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 10 #define meshtastic_ModuleConfig_PaxcounterConfig_size 30 -#define meshtastic_ModuleConfig_RangeTestConfig_size 10 +#define meshtastic_ModuleConfig_RangeTestConfig_size 12 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 42ebb8417..fb66dae7c 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels) JSONObject thisFileMap; thisFileMap["size"] = new JSONValue((int)file.size()); #ifdef ARCH_ESP32 - thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str()); + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #else - thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str()); + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #endif - if (String(file.name()).substring(1).endsWith(".gz")) { + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { #ifdef ARCH_ESP32 String modifiedFile = String(file.path()).substring(1); #else @@ -339,9 +342,15 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -362,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } else { @@ -371,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("Error"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } @@ -610,33 +621,35 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) res->println("
");
     }
 
+    // Helper lambda to create JSON array and clean up memory properly
+    auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * {
+        JSONArray tempArray;
+        for (int i = 0; i < count; i++) {
+            tempArray.push_back(new JSONValue((int)logArray[i]));
+        }
+        JSONValue *result = new JSONValue(tempArray);
+        // Note: Don't delete tempArray elements here - JSONValue now owns them
+        return result;
+    };
+
     // data->airtime->tx_log
-    JSONArray txLogValues;
     uint32_t *logArray;
     logArray = airTime->airtimeReport(TX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        txLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_log
-    JSONArray rxLogValues;
     logArray = airTime->airtimeReport(RX_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime->rx_all_log
-    JSONArray rxAllLogValues;
     logArray = airTime->airtimeReport(RX_ALL_LOG);
-    for (int i = 0; i < airTime->getPeriodsToLog(); i++) {
-        rxAllLogValues.push_back(new JSONValue((int)logArray[i]));
-    }
+    JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
 
     // data->airtime
     JSONObject jsonObjAirtime;
-    jsonObjAirtime["tx_log"] = new JSONValue(txLogValues);
-    jsonObjAirtime["rx_log"] = new JSONValue(rxLogValues);
-    jsonObjAirtime["rx_all_log"] = new JSONValue(rxAllLogValues);
+    jsonObjAirtime["tx_log"] = txLogJsonValue;
+    jsonObjAirtime["rx_log"] = rxLogJsonValue;
+    jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
     jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
     jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
     jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
@@ -646,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
     // data->wifi
     JSONObject jsonObjWifi;
     jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
-    jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str());
+    String wifiIPString = WiFi.localIP().toString();
+    std::string wifiIP = wifiIPString.c_str();
+    jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
 
     // data->memory
     JSONObject jsonObjMemory;
@@ -692,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
     jsonObjOuter["status"] = new JSONValue("ok");
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 }
 
@@ -763,8 +779,14 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     jsonObjOuter["status"] = new JSONValue("ok");
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
@@ -911,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
     JSONObject jsonObjOuter;
     jsonObjOuter["status"] = new JSONValue("ok");
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
 }
 
@@ -953,7 +976,13 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
 
     // serialize and write it to the stream
     JSONValue *value = new JSONValue(jsonObjOuter);
-    res->print(value->Stringify().c_str());
+    std::string jsonString = value->Stringify();
+    res->print(jsonString.c_str());
     delete value;
+
+    // Clean up the networkObjs to prevent memory leak
+    for (auto *val : networkObjs) {
+        delete val;
+    }
 }
 #endif
\ No newline at end of file
diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h
index 08c03dc6b..e4f65aa28 100644
--- a/src/mesh/mesh-pb-constants.h
+++ b/src/mesh/mesh-pb-constants.h
@@ -15,8 +15,27 @@
 // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in
 // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0]))
 #ifndef MAX_RX_TOPHONE
+#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
+#define MAX_RX_TOPHONE 8
+#else
 #define MAX_RX_TOPHONE 32
 #endif
+#endif
+
+/// max number of QueueStatus packets which can be waiting for delivery to phone
+#ifndef MAX_RX_QUEUESTATUS_TOPHONE
+#define MAX_RX_QUEUESTATUS_TOPHONE 2
+#endif
+
+/// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone
+#ifndef MAX_RX_MQTTPROXY_TOPHONE
+#define MAX_RX_MQTTPROXY_TOPHONE 8
+#endif
+
+/// max number of ClientNotification packets which can be waiting for delivery to phone
+#ifndef MAX_RX_NOTIFICATION_TOPHONE
+#define MAX_RX_NOTIFICATION_TOPHONE 2
+#endif
 
 /// Verify baseline assumption of node size. If it increases, we need to reevaluate
 /// the impact of its memory footprint, notably on MAX_NUM_NODES.
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index 4c893e462..407003f7e 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -505,7 +505,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
     if (mp.decoded.want_response && !myReply) {
         myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
     }
-
+    if (mp.pki_encrypted && myReply) {
+        myReply->pki_encrypted = true;
+    }
     return handled;
 }
 
@@ -941,6 +943,9 @@ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req)
         res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1012,6 +1017,9 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32
         res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1099,6 +1107,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
         res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag;
         setPassKey(&res);
         myReply = allocDataProtobuf(res);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1123,6 +1134,9 @@ void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &r
     }
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
@@ -1132,6 +1146,9 @@ void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
     r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag;
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req)
@@ -1200,6 +1217,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
     r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag;
     setPassKey(&r);
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex)
@@ -1211,6 +1231,9 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch
         r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag;
         setPassKey(&r);
         myReply = allocDataProtobuf(r);
+        if (req.pki_encrypted) {
+            myReply->pki_encrypted = true;
+        }
     }
 }
 
@@ -1220,6 +1243,9 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
     r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag;
     r.get_ui_config_response = uiconfig;
     myReply = allocDataProtobuf(r);
+    if (req.pki_encrypted) {
+        myReply->pki_encrypted = true;
+    }
 }
 
 void AdminModule::reboot(int32_t seconds)
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index d40dcd24f..2fc0bf4a6 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -632,10 +632,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
         // Normal canned message selection
         if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
         } else {
+#if CANNED_MESSAGE_ADD_CONFIRMATION
             // Show confirmation dialog before sending canned message
             NodeNum destNode = dest;
             ChannelIndex chan = channel;
-#if CANNED_MESSAGE_ADD_CONFIRMATION
             graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() {
                 this->sendText(destNode, chan, current, false);
                 payload = runState;
@@ -991,7 +991,6 @@ int32_t CannedMessageModule::runOnce()
                 this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
             }
         }
-        e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
         this->currentMessageIndex = -1;
         this->freetext = "";
         this->cursor = 0;
@@ -1433,10 +1432,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
                 meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
                 if (node) {
                     if (node->is_favorite) {
+#if defined(M5STACK_UNITC6L)
+                        snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name);
+                    } else {
+                        snprintf(entryText, sizeof(entryText), "%s", node->user.short_name);
+                    }
+#else
                         snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name);
                     } else {
                         snprintf(entryText, sizeof(entryText), "%s", node->user.long_name);
                     }
+#endif
                 }
             }
         }
@@ -1607,7 +1613,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
         int yOffset = y + 10;
 #else
         display->setFont(FONT_MEDIUM);
+#if defined(M5STACK_UNITC6L)
+        int yOffset = y;
+#else
         int yOffset = y + 10;
+#endif
 #endif
 
         // --- Delivery Status Message ---
@@ -1632,13 +1642,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
         }
 
         display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
+#if defined(M5STACK_UNITC6L)
+        yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding
+#else
         yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding
-
+#endif
 #ifndef USE_EINK
         // --- SNR + RSSI Compact Line ---
         if (this->ack) {
             display->setFont(FONT_SMALL);
+#if defined(M5STACK_UNITC6L)
+            snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi);
+#else
             snprintf(buffer, sizeof(buffer), "SNR: %.1f dB   RSSI: %d", this->lastRxSnr, this->lastRxRssi);
+#endif
             display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
         }
 #endif
diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp
index 1f871f87e..2f2934984 100644
--- a/src/modules/ExternalNotificationModule.cpp
+++ b/src/modules/ExternalNotificationModule.cpp
@@ -364,9 +364,10 @@ ExternalNotificationModule::ExternalNotificationModule()
     // moduleConfig.external_notification.alert_message_buzzer = true;
 
     if (moduleConfig.external_notification.enabled) {
+#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER)
         if (inputBroker) // put our callback in the inputObserver list
             inputObserver.observe(inputBroker);
-
+#endif
         if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig),
                               &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) {
             memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone));
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 3528f57f5..757753d45 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -3,11 +3,16 @@
 #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/TrackballInterruptImpl1.h"
 #include "input/UpDownInterruptImpl1.h"
+#include "input/i2cButton.h"
 #include "modules/SystemCommandsModule.h"
+#if HAS_TRACKBALL
+#include "input/TrackballInterruptImpl1.h"
+#endif
+
 #if !MESHTASTIC_EXCLUDE_I2C
 #include "input/cardKbI2cImpl.h"
 #endif
@@ -87,7 +92,7 @@
 #include "modules/StoreForwardModule.h"
 #endif
 #endif
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)
+
 #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
 #include "modules/ExternalNotificationModule.h"
 #endif
@@ -97,7 +102,6 @@
 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL
 #include "modules/SerialModule.h"
 #endif
-#endif
 
 #if !MESHTASTIC_EXCLUDE_DROPZONE
 #include "modules/DropzoneModule.h"
@@ -135,13 +139,20 @@ void setupModules()
         traceRouteModule = new TraceRouteModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_NEIGHBORINFO
-        neighborInfoModule = new NeighborInfoModule();
+        if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) {
+            neighborInfoModule = new NeighborInfoModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
-        detectionSensorModule = new DetectionSensorModule();
+        if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) {
+            detectionSensorModule = new DetectionSensorModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_ATAK
-        atakPluginModule = new AtakPluginModule();
+        if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TAK,
+                      meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) {
+            atakPluginModule = new AtakPluginModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_PKI
         keyVerificationModule = new KeyVerificationModule();
@@ -170,13 +181,25 @@ void setupModules()
                 delete rotaryEncoderInterruptImpl1;
                 rotaryEncoderInterruptImpl1 = nullptr;
             }
+#ifdef 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;
+            }
+#else
             upDownInterruptImpl1 = new UpDownInterruptImpl1();
             if (!upDownInterruptImpl1->init()) {
                 delete upDownInterruptImpl1;
                 upDownInterruptImpl1 = 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();
@@ -198,7 +221,7 @@ void setupModules()
             aLinuxInputImpl->init();
         }
 #endif
-#if !MESHTASTIC_EXCLUDE_INPUTBROKER
+#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);
@@ -218,11 +241,14 @@ void setupModules()
 #if HAS_TELEMETRY
         new DeviceTelemetryModule();
 #endif
-// TODO: How to improve this?
 #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
-        new EnvironmentTelemetryModule();
+        if (moduleConfig.has_telemetry &&
+            (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
+            new EnvironmentTelemetryModule();
+        }
 #if __has_include("Adafruit_PM25AQI.h")
-        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
+        if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled &&
+            nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
             new AirQualityTelemetryModule();
         }
 #endif
@@ -234,12 +260,16 @@ void setupModules()
 #endif
 #endif
 #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
-        new PowerTelemetryModule();
+        if (moduleConfig.has_telemetry &&
+            (moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) {
+            new PowerTelemetryModule();
+        }
 #endif
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 #if !MESHTASTIC_EXCLUDE_SERIAL
-        if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+        if (moduleConfig.has_serial && moduleConfig.serial.enabled &&
+            config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
             new SerialModule();
         }
 #endif
@@ -250,21 +280,26 @@ void setupModules()
         audioModule = new AudioModule();
 #endif
 #if !MESHTASTIC_EXCLUDE_PAXCOUNTER
-        paxcounterModule = new PaxcounterModule();
+        if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
+            paxcounterModule = new PaxcounterModule();
+        }
 #endif
 #endif
 #if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
 #if !MESHTASTIC_EXCLUDE_STOREFORWARD
-        storeForwardModule = new StoreForwardModule();
+        if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) {
+            storeForwardModule = new StoreForwardModule();
+        }
 #endif
 #endif
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)
 #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
-        externalNotificationModule = new ExternalNotificationModule();
+        if (moduleConfig.has_external_notification && moduleConfig.external_notification.enabled) {
+            externalNotificationModule = new ExternalNotificationModule();
+        }
 #endif
 #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
-        new RangeTestModule();
-#endif
+        if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
+            new RangeTestModule();
 #endif
     } else {
 #if !MESHTASTIC_EXCLUDE_ADMIN
diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp
index eebf428a4..97dc17001 100644
--- a/src/modules/NeighborInfoModule.cpp
+++ b/src/modules/NeighborInfoModule.cpp
@@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies)
 {
     meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
     collectNeighborInfo(&neighborInfo);
-    meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
-    // send regardless of whether or not we have neighbors in our DB,
-    // because we want to get neighbors for the next cycle
-    p->to = dest;
-    p->decoded.want_response = wantReplies;
-    p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
-    printNeighborInfo("SENDING", &neighborInfo);
-    service->sendToMesh(p, RX_SRC_LOCAL, true);
+    // only send neighbours if we have some to send
+    if (neighborInfo.neighbors_count > 0) {
+        meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo);
+        p->to = dest;
+        p->decoded.want_response = wantReplies;
+        p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
+        printNeighborInfo("SENDING", &neighborInfo);
+        service->sendToMesh(p, RX_SRC_LOCAL, true);
+    }
 }
 
 /*
@@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen
         neighbors.push_back(new_nbr);
     }
     return &neighbors.back();
-}
\ No newline at end of file
+}
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index 0060e99fa..276a11b3a 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule;
 
 bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
 {
-    auto p = *pptr;
-
     if (mp.from == nodeDB->getNodeNum()) {
         LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
         return false;
     }
+
+    auto p = *pptr;
     if (p.is_licensed != owner.is_licensed) {
         LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
         return true;
@@ -44,7 +44,10 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
     if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
         service->cancelSending(prevPacketId);
     shorterTimeout = _shorterTimeout;
+    DEBUG_HEAP_BEFORE;
     meshtastic_MeshPacket *p = allocReply();
+    DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p);
+
     if (p) { // Check whether we didn't ignore it
         p->to = dest;
         p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp
index 6f3d69acf..d1d2d9ead 100644
--- a/src/modules/RangeTestModule.cpp
+++ b/src/modules/RangeTestModule.cpp
@@ -31,7 +31,7 @@ uint32_t packetSequence = 0;
 
 int32_t RangeTestModule::runOnce()
 {
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
+#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
 
     /*
         Uncomment the preferences below if you want to use the module
@@ -130,7 +130,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies)
 
 ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp)
 {
-#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO)
+#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO)
 
     if (moduleConfig.range_test.enabled) {
 
diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp
index b10413cc8..e7e92c79a 100644
--- a/src/modules/RoutingModule.cpp
+++ b/src/modules/RoutingModule.cpp
@@ -73,7 +73,7 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit
     return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit
 }
 
-RoutingModule::RoutingModule() : ProtobufModule(ROUTING_MODULE, meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg)
+RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg)
 {
     isPromiscuous = true;
 
diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h
index 7b43a6e98..c047f6e29 100644
--- a/src/modules/RoutingModule.h
+++ b/src/modules/RoutingModule.h
@@ -2,8 +2,6 @@
 #include "Channels.h"
 #include "ProtobufModule.h"
 
-static const char *ROUTING_MODULE = "routing";
-
 /**
  * Routing module for router control messages
  */
diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp
index 866497ecc..7485f1c2d 100644
--- a/src/modules/SerialModule.cpp
+++ b/src/modules/SerialModule.cpp
@@ -45,9 +45,12 @@
 
 
 */
+#ifdef HELTEC_MESH_SOLAR
+#include "meshSolarApp.h"
+#endif
 
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 
 #define RX_BUFFER 256
 #define TIMEOUT 250
@@ -60,11 +63,11 @@
 SerialModule *serialModule;
 SerialModuleRadio *serialModuleRadio;
 
-#if defined(TTGO_T_ECHO) || defined(T_ECHO_LITE) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) ||  \
-    defined(ELECROW_ThinkNode_M5)
+#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) ||                          \
+    defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE)
 SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {}
 static Print *serialPrint = &Serial;
-#elif defined(CONFIG_IDF_TARGET_ESP32C6)
+#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172)
 SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") {}
 static Print *serialPrint = &Serial1;
 #else
@@ -78,7 +81,8 @@ size_t serialPayloadSize;
 bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config)
 {
     if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA,
-                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) {
+                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO,
+                                                          meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
         const char *warning =
             "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes.";
         LOG_ERROR(warning);
@@ -169,7 +173,18 @@ int32_t SerialModule::runOnce()
                 Serial.begin(baud);
                 Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
             }
-
+#elif defined(ARCH_STM32WL)
+#ifndef RAK3172
+            HardwareSerial *serialInstance = &Serial2;
+#else
+            HardwareSerial *serialInstance = &Serial1;
+#endif
+            if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
+                serialInstance->setTx(moduleConfig.serial.txd);
+                serialInstance->setRx(moduleConfig.serial.rxd);
+            }
+            serialInstance->begin(baud);
+            serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT);
 #elif defined(ARCH_ESP32)
 
             if (moduleConfig.serial.rxd && moduleConfig.serial.txd) {
@@ -241,13 +256,28 @@ int32_t SerialModule::runOnce()
             else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) {
                 processWXSerial();
 
-            } else {
+            }
+#if defined(HELTEC_MESH_SOLAR)
+            else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) {
+                serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1);
+                // If the parsing fails, the following parsing will be performed.
+                if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) {
+                    return runOncePart(serialBytes, serialPayloadSize);
+                }
+            }
+#endif
+            else {
 #if defined(CONFIG_IDF_TARGET_ESP32C6)
                 while (Serial1.available()) {
                     serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
 #else
-                while (Serial2.available()) {
-                    serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
+#ifndef RAK3172
+                HardwareSerial *serialInstance = &Serial2;
+#else
+                HardwareSerial *serialInstance = &Serial1;
+#endif
+                while (serialInstance->available()) {
+                    serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
 #endif
                     serialModuleRadio->sendPayload();
                 }
@@ -497,7 +527,7 @@ ParsedLine parseLine(const char *line)
 void SerialModule::processWXSerial()
 {
 #if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) &&              \
-    !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5)
+    !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL)
     static unsigned int lastAveraged = 0;
     static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded.
     static double dir_sum_sin = 0;
diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h
index 1c74c927c..dbe4f75db 100644
--- a/src/modules/SerialModule.h
+++ b/src/modules/SerialModule.h
@@ -8,8 +8,8 @@
 #include 
 #include 
 
-#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) &&               \
-    !defined(CONFIG_IDF_TARGET_ESP32C3)
+#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) &&                             \
+    !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
 
 class SerialModule : public StreamAPI, private concurrency::OSThread
 {
diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp
index 08fd09db0..98d5b19d0 100644
--- a/src/modules/Telemetry/DeviceTelemetry.cpp
+++ b/src/modules/Telemetry/DeviceTelemetry.cpp
@@ -172,7 +172,10 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
              telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
              telemetry.variant.device_metrics.uptime_seconds);
 
+    DEBUG_HEAP_BEFORE;
     meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
+    DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p);
+
     p->to = dest;
     p->decoded.want_response = false;
     p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index 8926b171c..c90d9250f 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -198,6 +198,13 @@ T1000xSensor t1000xSensor;
 IndicatorSensor indicatorSensor;
 #endif
 
+#if __has_include()
+#include "Sensor/TSL2561Sensor.h"
+TSL2561Sensor tsl2561Sensor;
+#else
+NullSensor tsl2561Sensor;
+#endif
+
 #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
 
@@ -296,6 +303,8 @@ int32_t EnvironmentTelemetryModule::runOnce()
                 result = max17048Sensor.runOnce();
             if (cgRadSens.hasSensor())
                 result = cgRadSens.runOnce();
+            if (tsl2561Sensor.hasSensor())
+                result = tsl2561Sensor.runOnce();
             if (pct2075Sensor.hasSensor())
                 result = pct2075Sensor.runOnce();
                 // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the
@@ -642,6 +651,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
         valid = valid && nau7802Sensor.getMetrics(m);
         hasSensor = true;
     }
+    if (tsl2561Sensor.hasSensor()) {
+        valid = valid && tsl2561Sensor.getMetrics(m);
+        hasSensor = true;
+    }
     if (aht10Sensor.hasSensor()) {
         if (!bmp280Sensor.hasSensor() && !bmp3xxSensor.hasSensor()) {
             valid = valid && aht10Sensor.getMetrics(m);
diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp
new file mode 100644
index 000000000..9f3b7e460
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.cpp
@@ -0,0 +1,41 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TSL2561Sensor.h"
+#include "TelemetrySensor.h"
+#include 
+
+TSL2561Sensor::TSL2561Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_TSL2561, "TSL2561") {}
+
+int32_t TSL2561Sensor::runOnce()
+{
+    LOG_INFO("Init sensor: %s", sensorName);
+    if (!hasSensor()) {
+        return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
+    }
+
+    status = tsl.begin(nodeTelemetrySensorsMap[sensorType].second);
+
+    return initI2CSensor();
+}
+
+void TSL2561Sensor::setup()
+{
+    tsl.setGain(TSL2561_GAIN_1X);
+    tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS);
+}
+
+bool TSL2561Sensor::getMetrics(meshtastic_Telemetry *measurement)
+{
+    measurement->variant.environment_metrics.has_lux = true;
+    sensors_event_t event;
+    tsl.getEvent(&event);
+    measurement->variant.environment_metrics.lux = event.light;
+    LOG_INFO("Lux: %f", measurement->variant.environment_metrics.lux);
+
+    return true;
+}
+
+#endif
diff --git a/src/modules/Telemetry/Sensor/TSL2561Sensor.h b/src/modules/Telemetry/Sensor/TSL2561Sensor.h
new file mode 100644
index 000000000..0329becd8
--- /dev/null
+++ b/src/modules/Telemetry/Sensor/TSL2561Sensor.h
@@ -0,0 +1,23 @@
+#include "configuration.h"
+
+#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include()
+
+#include "../mesh/generated/meshtastic/telemetry.pb.h"
+#include "TelemetrySensor.h"
+#include 
+
+class TSL2561Sensor : public TelemetrySensor
+{
+  private:
+    // The magic number is a sensor id, the actual value doesn't matter
+    Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_LOW, 12345);
+
+  protected:
+    virtual void setup() override;
+
+  public:
+    TSL2561Sensor();
+    virtual int32_t runOnce() override;
+    virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
+};
+#endif
diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h
index d0efa5ae6..ddca5767c 100755
--- a/src/motion/BMX160Sensor.h
+++ b/src/motion/BMX160Sensor.h
@@ -7,7 +7,7 @@
 
 #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
 
-#if defined(RAK_4631) && !defined(RAK2560) && __has_include()
+#if !defined(RAK2560) && __has_include()
 
 #include "Fusion/Fusion.h"
 #include 
diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp
index 95e191c8e..0eb8e9bdd 100644
--- a/src/nimble/NimbleBluetooth.cpp
+++ b/src/nimble/NimbleBluetooth.cpp
@@ -11,6 +11,12 @@
 #include 
 #include 
 
+#ifdef NIMBLE_TWO
+#include "NimBLEAdvertising.h"
+#include "NimBLEExtAdvertising.h"
+#include "PowerStatus.h"
+#endif
+
 NimBLECharacteristic *fromNumCharacteristic;
 NimBLECharacteristic *BatteryCharacteristic;
 NimBLECharacteristic *logRadioCharacteristic;
@@ -56,13 +62,18 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
     {
         PhoneAPI::onNowHasData(fromRadioNum);
 
-        LOG_DEBUG("BLE notify fromNum");
+        uint8_t cc = bleServer->getConnectedCount();
+        LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc);
 
         uint8_t val[4];
         put_le32(val, fromRadioNum);
 
         fromNumCharacteristic->setValue(val, sizeof(val));
+#ifdef NIMBLE_TWO
+        fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
+#else
         fromNumCharacteristic->notify();
+#endif
     }
 
     /// Check the current underlying physical link to see if the client is currently connected
@@ -79,7 +90,12 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
 
 class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
 {
+#ifdef NIMBLE_TWO
+    virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
+#else
     virtual void onWrite(NimBLECharacteristic *pCharacteristic)
+
+#endif
     {
         auto val = pCharacteristic->getValue();
 
@@ -97,7 +113,11 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
 
 class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
 {
+#ifdef NIMBLE_TWO
+    virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
+#else
     virtual void onRead(NimBLECharacteristic *pCharacteristic)
+#endif
     {
         int tries = 0;
         bluetoothPhoneAPI->phoneWants = true;
@@ -107,9 +127,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
             tries++;
         }
         std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);
-        std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes,
-                                        bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes);
-        pCharacteristic->setValue(fromRadioByteString);
+        pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes);
 
         if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload
             bluetoothPhoneAPI->setIntervalFromNow(0);
@@ -121,7 +139,17 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
 
 class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
 {
+#ifdef NIMBLE_TWO
+  public:
+    NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
+
+  private:
+    NimbleBluetooth *ble;
+
+    virtual uint32_t onPassKeyDisplay()
+#else
     virtual uint32_t onPassKeyRequest()
+#endif
     {
         uint32_t passkey = config.bluetooth.fixed_pin;
 
@@ -133,7 +161,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
 
         powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey)));
+        meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
+        bluetoothStatus->updateStatus(&newStatus);
 
 #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
         if (screen) {
@@ -169,11 +198,16 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         return passkey;
     }
 
+#ifdef NIMBLE_TWO
+    virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
+#else
     virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
+#endif
     {
         LOG_INFO("BLE authentication complete");
 
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+        meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+        bluetoothStatus->updateStatus(&newStatus);
 
         // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
         if (passkeyShowing) {
@@ -183,12 +217,23 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
         }
     }
 
+#ifdef NIMBLE_TWO
+    virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
+    {
+        LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
+    }
+
+    virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
+    {
+        LOG_INFO("BLE disconnect reason: %d", reason);
+#else
     virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
     {
         LOG_INFO("BLE disconnect");
+#endif
 
-        bluetoothStatus->updateStatus(
-            new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+        meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+        bluetoothStatus->updateStatus(&newStatus);
 
         if (bluetoothPhoneAPI) {
             std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex);
@@ -198,6 +243,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
             bluetoothPhoneAPI->numBytes = 0;
             bluetoothPhoneAPI->queue_size = 0;
         }
+#ifdef NIMBLE_TWO
+        // Restart Advertising
+        ble->startAdvertising();
+#endif
     }
 };
 
@@ -249,7 +298,11 @@ int NimbleBluetooth::getRssi()
     if (bleServer && isConnected()) {
         auto service = bleServer->getServiceByUUID(MESH_SERVICE_UUID);
         uint16_t handle = service->getHandle();
+#ifdef NIMBLE_TWO
+        return NimBLEDevice::getClientByHandle(handle)->getRssi();
+#else
         return NimBLEDevice::getClientByID(handle)->getRssi();
+#endif
     }
     return 0; // FIXME figure out where to source this
 }
@@ -271,8 +324,11 @@ void NimbleBluetooth::setup()
         NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
     }
     bleServer = NimBLEDevice::createServer();
-
+#ifdef NIMBLE_TWO
+    NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
+#else
     NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback();
+#endif
     bleServer->setCallbacks(serverCallbacks, true);
     setupService();
     startAdvertising();
@@ -316,8 +372,11 @@ void NimbleBluetooth::setupService()
     NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
     BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
         (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
-
+#ifdef NIMBLE_TWO
+    NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
+#else
     NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
+#endif
     batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
     batteryLevelDescriptor->setNamespace(1);
     batteryLevelDescriptor->setUnit(0x27ad);
@@ -327,11 +386,40 @@ void NimbleBluetooth::setupService()
 
 void NimbleBluetooth::startAdvertising()
 {
+#ifdef NIMBLE_TWO
+    NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
+    NimBLEExtAdvertisement legacyAdvertising;
+
+    legacyAdvertising.setLegacyAdvertising(true);
+    legacyAdvertising.setScannable(true);
+    legacyAdvertising.setConnectable(true);
+    legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN);
+    if (powerStatus->getHasBattery() == 1) {
+        legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f));
+    }
+    legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID));
+    legacyAdvertising.setMinInterval(500);
+    legacyAdvertising.setMaxInterval(1000);
+
+    NimBLEExtAdvertisement legacyScanResponse;
+    legacyScanResponse.setLegacyAdvertising(true);
+    legacyScanResponse.setConnectable(true);
+    legacyScanResponse.setName(getDeviceName());
+
+    if (!pAdvertising->setInstanceData(0, legacyAdvertising)) {
+        LOG_ERROR("BLE failed to set legacyAdvertising");
+    } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) {
+        LOG_ERROR("BLE failed to set legacyScanResponse");
+    } else if (!pAdvertising->start(0, 0, 0)) {
+        LOG_ERROR("BLE failed to start legacyAdvertising");
+    }
+#else
     NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
     pAdvertising->reset();
     pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
     pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
     pAdvertising->start(0);
+#endif
 }
 
 /// Given a level between 0-100, update the BLE attribute
@@ -339,7 +427,11 @@ void updateBatteryLevel(uint8_t level)
 {
     if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
         BatteryCharacteristic->setValue(&level, 1);
+#ifdef NIMBLE_TWO
+        BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
+#else
         BatteryCharacteristic->notify();
+#endif
     }
 }
 
@@ -354,7 +446,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
     if (!bleServer || !isConnected() || length > 512) {
         return;
     }
+#ifdef NIMBLE_TWO
+    logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
+#else
     logRadioCharacteristic->notify(logMessage, length, true);
+#endif
 }
 
 void clearNVS()
@@ -364,4 +460,4 @@ void clearNVS()
     ESP.restart();
 #endif
 }
-#endif
+#endif
\ No newline at end of file
diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h
index 45602e088..899355b4d 100644
--- a/src/nimble/NimbleBluetooth.h
+++ b/src/nimble/NimbleBluetooth.h
@@ -12,10 +12,15 @@ class NimbleBluetooth : BluetoothApi
     bool isConnected();
     int getRssi();
     void sendLog(const uint8_t *logMessage, size_t length);
+#if defined(NIMBLE_TWO)
+    void startAdvertising();
+#endif
 
   private:
     void setupService();
+#if !defined(NIMBLE_TWO)
     void startAdvertising();
+#endif
 };
 
 void setBluetoothEnable(bool enable);
diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h
index 522e862ac..22ce6487f 100644
--- a/src/platform/esp32/architecture.h
+++ b/src/platform/esp32/architecture.h
@@ -192,6 +192,10 @@
 #define HW_VENDOR meshtastic_HardwareModel_LINK_32
 #elif defined(T_DECK_PRO)
 #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
+#elif defined(T_LORA_PAGER)
+#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
+#elif defined(M5STACK_UNITC6L)
+#define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L
 #endif
 
 // -----------------------------------------------------------------------------
diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp
new file mode 100644
index 000000000..ea5773d30
--- /dev/null
+++ b/src/platform/extra_variants/t_lora_pager/variant.cpp
@@ -0,0 +1,27 @@
+#include "configuration.h"
+
+#ifdef T_LORA_PAGER
+
+#include "AudioBoard.h"
+
+DriverPins PinsAudioBoardES8311;
+AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
+
+// TLora Pager specific init
+void lateInitVariant()
+{
+    // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug);
+    // I2C: function, scl, sda
+    PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire);
+    // I2S: function, mclk, bck, ws, data_out, data_in
+    PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN);
+
+    // configure codec
+    CodecConfig cfg;
+    cfg.input_device = ADC_INPUT_LINE1;
+    cfg.output_device = DAC_OUTPUT_ALL;
+    cfg.i2s.bits = BIT_LENGTH_16BITS;
+    cfg.i2s.rate = RATE_44K;
+    board.begin(cfg);
+}
+#endif
\ No newline at end of file
diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp
index 6f0e7250f..f8366ae32 100644
--- a/src/platform/nrf52/NRF52Bluetooth.cpp
+++ b/src/platform/nrf52/NRF52Bluetooth.cpp
@@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle)
     LOG_INFO("BLE Connected to %s", central_name);
 
     // Notify UI (or any other interested firmware components)
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+    meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+    bluetoothStatus->updateStatus(&newStatus);
 }
 /**
  * Callback invoked when a connection is dropped
@@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason)
     }
 
     // Notify UI (or any other interested firmware components)
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+    meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+    bluetoothStatus->updateStatus(&newStatus);
 }
 void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value)
 {
@@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
         textkey += (char)passkey[i];
 
     // Notify UI (or other components) of pairing event and passkey
-    bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey));
+    meshtastic::BluetoothStatus newStatus(textkey);
+    bluetoothStatus->updateStatus(&newStatus);
 
 #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
     if (screen) {
@@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu
 {
     if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
         LOG_INFO("BLE pair success");
-        bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED));
+        meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
+        bluetoothStatus->updateStatus(&newConnectedStatus);
     } else {
         LOG_INFO("BLE pair failed");
         // Notify UI (or any other interested firmware components)
-        bluetoothStatus->updateStatus(
-            new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED));
+        meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
+        bluetoothStatus->updateStatus(&newDisconnectedStatus);
     }
 
     // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h
index 064bd8ef0..c9938062e 100644
--- a/src/platform/nrf52/architecture.h
+++ b/src/platform/nrf52/architecture.h
@@ -98,6 +98,8 @@
 #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK
 #elif defined(SEEED_WIO_TRACKER_L1)
 #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1
+#elif defined(HELTEC_MESH_SOLAR)
+#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR
 #else
 #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN
 #endif
diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp
index 590d2f0ae..8ce74d5f7 100644
--- a/src/platform/nrf52/main-nrf52.cpp
+++ b/src/platform/nrf52/main-nrf52.cpp
@@ -323,7 +323,7 @@ void cpuDeepSleep(uint32_t msecToWake)
 #endif
 #endif
 
-#ifdef HELTEC_MESH_NODE_T114
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR)
     nrf_gpio_cfg_default(PIN_GPS_PPS);
     detachInterrupt(PIN_GPS_PPS);
     detachInterrupt(PIN_BUTTON1);
diff --git a/src/power.h b/src/power.h
index 1c078c06d..e96f5b022 100644
--- a/src/power.h
+++ b/src/power.h
@@ -128,6 +128,8 @@ class Power : private concurrency::OSThread
     bool lipoInit();
     /// Setup a Lipo charger
     bool lipoChargerInit();
+    /// Setup a meshSolar battery sensor
+    bool meshSolarInit();
 
   private:
     void shutdown();
diff --git a/src/sleep.cpp b/src/sleep.cpp
index 1a5f246c5..83597e349 100644
--- a/src/sleep.cpp
+++ b/src/sleep.cpp
@@ -32,6 +32,16 @@ esp_sleep_source_t wakeCause; // the reason we booted this time
 #endif
 #include "Throttle.h"
 
+#ifdef USE_XL9555
+#include "ExtensionIOXL9555.hpp"
+extern ExtensionIOXL9555 io;
+#endif
+
+#ifdef HAS_PPM
+#include 
+extern XPowersPPM *PPM;
+#endif
+
 #ifndef INCLUDE_vTaskSuspend
 #define INCLUDE_vTaskSuspend 0
 #endif
@@ -297,6 +307,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
 #endif
 #endif
 
+#ifdef HAS_PPM
+    if (PPM) {
+        LOG_INFO("PMM shutdown");
+        console->flush();
+        PPM->shutdown();
+    }
+#endif
+
 #ifdef HAS_PMU
     if (pmu_found && PMU) {
         // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep.
@@ -412,6 +430,7 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
     if (pmu_found)
         gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
 #endif
+
     auto res = esp_sleep_enable_gpio_wakeup();
     if (res != ESP_OK) {
         LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
index 557ee7a49..37cfc1626 100644
--- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp
+++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
@@ -1,20 +1,9 @@
 #include "../test_helpers.h"
 
-// Test encrypted packet serialization
-void test_encrypted_packet_serialization()
+// Helper function for all encrypted packet assertions
+void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
 {
-    meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
-    packet.from = 0x11223344;
-    packet.to = 0x55667788;
-    packet.id = 0x9999;
-    packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
-
-    // Add some dummy encrypted data
-    const char *encrypted_data = "encrypted_payload_data";
-    packet.encrypted.size = strlen(encrypted_data);
-    memcpy(packet.encrypted.bytes, encrypted_data, packet.encrypted.size);
-
-    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+    // Parse and validate JSON
     TEST_ASSERT_TRUE(json.length() > 0);
 
     JSONValue *root = JSON::Parse(json.c_str());
@@ -23,28 +12,48 @@ void test_encrypted_packet_serialization()
 
     JSONObject jsonObj = root->AsObject();
 
-    // Check basic packet fields
+    // Assert basic packet fields
     TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber());
+    TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
 
-    // Check that it has encrypted data fields (not "payload" but "bytes" and "size")
+    // Assert encrypted data fields
     TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
-    TEST_ASSERT_TRUE(jsonObj["bytes"]->IsString());
+    TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
 
     TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
-    TEST_ASSERT_EQUAL(22, (int)jsonObj["size"]->AsNumber()); // strlen("encrypted_payload_data") = 22
+    TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
 
-    // The encrypted data should be hex-encoded
+    // Assert hex encoding
     std::string encrypted_hex = jsonObj["bytes"]->AsString();
-    TEST_ASSERT_TRUE(encrypted_hex.length() > 0);
-    // Should be twice the size of the original data (hex encoding)
-    TEST_ASSERT_EQUAL(44, encrypted_hex.length()); // 22 * 2 = 44
+    TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
 
     delete root;
 }
+
+// Test encrypted packet serialization
+void test_encrypted_packet_serialization()
+{
+    const char *data = "encrypted_payload_data";
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(data), strlen(data),
+                           meshtastic_MeshPacket_encrypted_tag);
+    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+
+    assert_encrypted_packet(json, packet);
+}
+
+// Test empty encrypted packet
+void test_empty_encrypted_packet()
+{
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0, meshtastic_MeshPacket_encrypted_tag);
+    std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet);
+
+    assert_encrypted_packet(json, packet);
+}
diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp
index de3f34541..0f3b0bc6d 100644
--- a/test/test_meshpacket_serializer/ports/test_text_message.cpp
+++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp
@@ -1,42 +1,105 @@
 #include "../test_helpers.h"
+#include 
+
+// Helper function to test common packet fields and structure
+void verify_text_message_packet_structure(const std::string &json, const char *expected_text)
+{
+    TEST_ASSERT_TRUE(json.length() > 0);
+
+    // Use smart pointer for automatic memory management
+    std::unique_ptr root(JSON::Parse(json.c_str()));
+    TEST_ASSERT_NOT_NULL(root.get());
+    TEST_ASSERT_TRUE(root->IsObject());
+
+    JSONObject jsonObj = root->AsObject();
+
+    // Check basic packet fields - use helper function to reduce duplication
+    auto check_field = [&](const char *field, uint32_t expected_value) {
+        auto it = jsonObj.find(field);
+        TEST_ASSERT_TRUE(it != jsonObj.end());
+        TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber());
+    };
+
+    check_field("from", 0x11223344);
+    check_field("to", 0x55667788);
+    check_field("id", 0x9999);
+
+    // Check message type
+    auto type_it = jsonObj.find("type");
+    TEST_ASSERT_TRUE(type_it != jsonObj.end());
+    TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str());
+
+    // Check payload
+    auto payload_it = jsonObj.find("payload");
+    TEST_ASSERT_TRUE(payload_it != jsonObj.end());
+    TEST_ASSERT_TRUE(payload_it->second->IsObject());
+
+    JSONObject payload = payload_it->second->AsObject();
+    auto text_it = payload.find("text");
+    TEST_ASSERT_TRUE(text_it != payload.end());
+    TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str());
+
+    // No need for manual delete with smart pointer
+}
 
 // Test TEXT_MESSAGE_APP port
 void test_text_message_serialization()
 {
     const char *test_text = "Hello Meshtastic!";
     meshtastic_MeshPacket packet =
-        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, (const uint8_t *)test_text, strlen(test_text));
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text));
 
     std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
-    TEST_ASSERT_TRUE(json.length() > 0);
-
-    JSONValue *root = JSON::Parse(json.c_str());
-    TEST_ASSERT_NOT_NULL(root);
-    TEST_ASSERT_TRUE(root->IsObject());
-
-    JSONObject jsonObj = root->AsObject();
-
-    // Check basic packet fields
-    TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber());
-
-    TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber());
-
-    // Check message type
-    TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end());
-    TEST_ASSERT_EQUAL_STRING("text", jsonObj["type"]->AsString().c_str());
-
-    // Check payload
-    TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end());
-    TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject());
-
-    JSONObject payload = jsonObj["payload"]->AsObject();
-    TEST_ASSERT_TRUE(payload.find("text") != payload.end());
-    TEST_ASSERT_EQUAL_STRING("Hello Meshtastic!", payload["text"]->AsString().c_str());
-
-    delete root;
+    verify_text_message_packet_structure(json, test_text);
 }
+
+// Test with nullptr to check robustness
+void test_text_message_serialization_null()
+{
+    meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0);
+
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    verify_text_message_packet_structure(json, "");
+}
+
+// Test TEXT_MESSAGE_APP port with very long message (boundary testing)
+void test_text_message_serialization_long_text()
+{
+    // Test with actual message size limits
+    constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit
+    std::string long_text(MAX_MESSAGE_SIZE, 'A');
+
+    meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP,
+                                                      reinterpret_cast(long_text.c_str()), long_text.length());
+
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    verify_text_message_packet_structure(json, long_text.c_str());
+}
+
+// Test with message over size limit (should fail)
+void test_text_message_serialization_oversized()
+{
+    constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit
+    std::string oversized_text(OVERSIZED_MESSAGE, 'B');
+
+    meshtastic_MeshPacket packet = create_test_packet(
+        meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length());
+
+    // Should fail or return empty/error
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    // Should only verify first 234 characters for oversized messages
+    std::string expected_text = oversized_text.substr(0, 234);
+    verify_text_message_packet_structure(json, expected_text.c_str());
+}
+
+// Add test for malformed UTF-8 sequences
+void test_text_message_serialization_invalid_utf8()
+{
+    const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8
+    meshtastic_MeshPacket packet =
+        create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1);
+
+    // Should not crash, may produce replacement characters
+    std::string json = MeshPacketSerializer::JsonSerialize(&packet, false);
+    TEST_ASSERT_TRUE(json.length() > 0);
+}
\ No newline at end of file
diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h
index 630e059bc..12245b85d 100644
--- a/test/test_meshpacket_serializer/test_helpers.h
+++ b/test/test_meshpacket_serializer/test_helpers.h
@@ -11,7 +11,8 @@
 #include 
 
 // Helper function to create a test packet with the given port and payload
-static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size)
+static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size,
+                                                int payload_variant = meshtastic_MeshPacket_decoded_tag)
 {
     meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
 
@@ -29,8 +30,12 @@ static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const u
     packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY;
 
     // Set decoded variant
-    packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
+    packet.which_payload_variant = payload_variant;
     packet.decoded.portnum = port;
+    if (payload_variant == meshtastic_MeshPacket_encrypted_tag && payload) {
+        packet.encrypted.size = payload_size;
+        memcpy(packet.encrypted.bytes, payload, packet.encrypted.size);
+    }
     memcpy(packet.decoded.payload.bytes, payload, payload_size);
     packet.decoded.payload.size = payload_size;
     packet.decoded.want_response = false;
diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp
index d74031fa4..484db8d74 100644
--- a/test/test_meshpacket_serializer/test_serializer.cpp
+++ b/test/test_meshpacket_serializer/test_serializer.cpp
@@ -4,6 +4,10 @@
 
 // Forward declarations for test functions
 void test_text_message_serialization();
+void test_text_message_serialization_null();
+void test_text_message_serialization_long_text();
+void test_text_message_serialization_oversized();
+void test_text_message_serialization_invalid_utf8();
 void test_position_serialization();
 void test_nodeinfo_serialization();
 void test_waypoint_serialization();
@@ -14,6 +18,7 @@ void test_telemetry_environment_metrics_missing_fields();
 void test_telemetry_environment_metrics_complete_coverage();
 void test_telemetry_environment_metrics_unset_fields();
 void test_encrypted_packet_serialization();
+void test_empty_encrypted_packet();
 
 void setup()
 {
@@ -21,6 +26,10 @@ void setup()
 
     // Text message tests
     RUN_TEST(test_text_message_serialization);
+    RUN_TEST(test_text_message_serialization_null);
+    RUN_TEST(test_text_message_serialization_long_text);
+    RUN_TEST(test_text_message_serialization_oversized);
+    RUN_TEST(test_text_message_serialization_invalid_utf8);
 
     // Position tests
     RUN_TEST(test_position_serialization);
@@ -41,6 +50,7 @@ void setup()
 
     // Encrypted packet test
     RUN_TEST(test_encrypted_packet_serialization);
+    RUN_TEST(test_empty_encrypted_packet);
 
     UNITY_END();
 }
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
new file mode 100644
index 000000000..809599212
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini
@@ -0,0 +1,12 @@
+; 9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+; https://shopee.com.my/product/1095224/21692283917
+[env:9m2ibr_aprs_lora_tracker]
+extends = esp32_base
+board = esp32doit-devkit-v1
+board_level = extra
+build_flags =
+  ${esp32_base.build_flags}
+  -D PRIVATE_HW
+  -D EBYTE_E22
+  -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation
+  -I variants/esp32/diy/9m2ibr_aprs_lora_tracker
diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
new file mode 100644
index 000000000..037933140
--- /dev/null
+++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h
@@ -0,0 +1,74 @@
+/*
+
+  9M2IBR APRS LoRa Tracker: ESP32-WROOM-32 + EBYTE E22-400M30S
+  https://shopee.com.my/product/1095224/21692283917
+
+  Originally developed for LoRa_APRS_iGate and GPIO is similar to
+  https://github.com/richonguzman/LoRa_APRS_iGate/blob/main/variants/ESP32_DIY_1W_LoRa_Mesh_V1_2/board_pinout.h
+
+*/
+
+// OLED (may be different controllers depending on screen size)
+#define I2C_SDA 21
+#define I2C_SCL 22
+#define HAS_SCREEN 1 // Generates randomized BLE pin
+
+// GNSS: Ai-Thinker GP-02 BDS/GNSS module
+#define GPS_RX_PIN 16
+#define GPS_TX_PIN 17
+
+// Button
+#define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client
+
+// LEDs
+#define LED_PIN 13 // Tx LED
+#define USER_LED 2 // Rx LED
+
+// Buzzer
+#define PIN_BUZZER 33
+
+// Battery sense
+#define BATTERY_PIN 35
+#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance
+#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
+#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION
+
+// SPI
+#define LORA_SCK 18
+#define LORA_MISO 19
+#define LORA_MOSI 23
+
+// LoRa
+#define LORA_CS 5
+#define LORA_DIO0 26          // a No connect on the SX1262/SX1268 module
+#define LORA_RESET 27         // RST for SX1276, and for SX1262/SX1268
+#define LORA_DIO1 12          // IRQ for SX1262/SX1268
+#define LORA_DIO2 RADIOLIB_NC // BUSY for SX1262/SX1268
+#define LORA_DIO3             // NC, but used as TCXO supply by E22 module
+#define LORA_RXEN 32          // RF switch RX (and E22 LNA) control by ESP32 GPIO
+#define LORA_TXEN 25          // RF switch TX (and E22 PA) control by ESP32 GPIO
+
+// RX/TX for RFM95/SX127x
+#define RF95_RXEN LORA_RXEN
+#define RF95_TXEN LORA_TXEN
+// #define RF95_TCXO 
+
+// common pinouts for SX126X modules
+#define SX126X_CS 5
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_DIO2
+#define SX126X_RESET LORA_RESET
+#define SX126X_RXEN LORA_RXEN
+#define SX126X_TXEN LORA_TXEN
+
+// Support alternative modules if soldered in place of E22
+#define USE_RF95 // RFM95/SX127x
+#define USE_SX1262
+#define USE_SX1268
+#define USE_LLCC68
+
+// E22 TCXO support
+#ifdef EBYTE_E22
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL
+#endif
diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini
index 60e686f9e..93c3e3394 100644
--- a/variants/esp32/heltec_wireless_bridge/platformio.ini
+++ b/variants/esp32/heltec_wireless_bridge/platformio.ini
@@ -1,6 +1,7 @@
 [env:heltec-wireless-bridge]
 ;build_type = debug ; to make it possible to step through our jtag debugger 
 extends = esp32_base
+board_level = extra
 board = heltec_wifi_lora_32
 build_flags = 
   ${esp32_base.build_flags}
diff --git a/variants/esp32/trackerd/platformio.ini b/variants/esp32/trackerd/platformio.ini
index 3c2726a3c..00c14fad2 100644
--- a/variants/esp32/trackerd/platformio.ini
+++ b/variants/esp32/trackerd/platformio.ini
@@ -1,5 +1,6 @@
 [env:trackerd]
 extends = esp32_base
+board_level = extra
 board = pico32
 board_build.f_flash = 80000000L
 
diff --git a/variants/esp32c6/m5stack_unitc6l/pins_arduino.h b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h
new file mode 100644
index 000000000..5b169a2d4
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h
@@ -0,0 +1,28 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include 
+
+#define USB_VID 0x2886
+#define USB_PID 0x0048
+
+static const uint8_t TX = 16;
+static const uint8_t RX = 17;
+
+static const uint8_t SDA = 10;
+static const uint8_t SCL = 8;
+
+// Default SPI will be mapped to Radio
+static const uint8_t MISO = 22;
+static const uint8_t SCK = 20;
+static const uint8_t MOSI = 21;
+static const uint8_t SS = 6;
+
+// #define SPI_MOSI (11)
+// #define SPI_SCK (14)
+// #define SPI_MISO (2)
+// #define SPI_CS (13)
+
+// #define SDCARD_CS SPI_CS
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini
new file mode 100644
index 000000000..da1c70c0a
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini
@@ -0,0 +1,35 @@
+[env:m5stack-unitc6l]
+extends = esp32c6_base
+board = esp32-c6-devkitc-1
+;OpenOCD flash method
+;upload_protocol = esp-builtin
+;Normal method
+upload_protocol = esptool
+;upload_port = /dev/ttyACM2
+build_unflags =
+  -D HAS_BLUETOOTH
+  -D MESHTASTIC_EXCLUDE_BLUETOOTH
+  -D HAS_WIFI
+lib_deps =
+  ${esp32c6_base.lib_deps}
+  adafruit/Adafruit NeoPixel@^1.12.3
+  h2zero/NimBLE-Arduino@^2.3.6
+build_flags = 
+  ${esp32c6_base.build_flags}
+  -D M5STACK_UNITC6L
+  -I variants/esp32c6/m5stack_unitc6l
+  -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
+  -DARDUINO_USB_CDC_ON_BOOT=1
+  -DARDUINO_USB_MODE=1
+  -D HAS_BLUETOOTH=1
+  -D MESHTASTIC_EXCLUDE_WEBSERVER
+  -D MESHTASTIC_EXCLUDE_MQTT
+	-DCONFIG_BT_NIMBLE_EXT_ADV=1
+	-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
+  -D NIMBLE_TWO
+monitor_speed=115200
+lib_ignore =
+  NonBlockingRTTTL
+  libpax
+build_src_filter = 
+ ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l>
\ No newline at end of file
diff --git a/variants/esp32c6/m5stack_unitc6l/variant.cpp b/variants/esp32c6/m5stack_unitc6l/variant.cpp
new file mode 100644
index 000000000..8e26b4ab7
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/variant.cpp
@@ -0,0 +1,74 @@
+#include "driver/gpio.h"
+#include 
+#include 
+// I2C device addr
+#define PI4IO_M_ADDR 0x43
+
+// PI4IO registers
+#define PI4IO_REG_CHIP_RESET 0x01
+#define PI4IO_REG_IO_DIR 0x03
+#define PI4IO_REG_OUT_SET 0x05
+#define PI4IO_REG_OUT_H_IM 0x07
+#define PI4IO_REG_IN_DEF_STA 0x09
+#define PI4IO_REG_PULL_EN 0x0B
+#define PI4IO_REG_PULL_SEL 0x0D
+#define PI4IO_REG_IN_STA 0x0F
+#define PI4IO_REG_INT_MASK 0x11
+#define PI4IO_REG_IRQ_STA 0x13
+// PI4IO
+
+#define setbit(x, y) x |= (0x01 << y)
+#define clrbit(x, y) x &= ~(0x01 << y)
+#define reversebit(x, y) x ^= (0x01 << y)
+#define getbit(x, y) ((x) >> (y)&0x01)
+
+void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value)
+{
+    Wire.beginTransmission(addr);
+    Wire.write(reg);
+    Wire.endTransmission();
+    Wire.requestFrom(addr, 1);
+    *value = Wire.read();
+}
+
+/*******************************************************************/
+void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value)
+{
+    Wire.beginTransmission(addr);
+    Wire.write(reg);
+    Wire.write(value);
+    Wire.endTransmission();
+}
+/*******************************************************************/
+void c6l_init()
+{
+    // P7 LoRa Reset
+    // P6 RF Switch
+    // P5 LNA Enable
+
+    printf("pi4io_init\n");
+    uint8_t in_data;
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF);
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data);
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0
+    vTaskDelay(10 / portTICK_PERIOD_MS);
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志
+
+    i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data);
+    setbit(in_data, 6); // HIGH
+    i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data);
+}
diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h
new file mode 100644
index 000000000..d973aa281
--- /dev/null
+++ b/variants/esp32c6/m5stack_unitc6l/variant.h
@@ -0,0 +1,52 @@
+void c6l_init();
+
+#define HAS_GPS 1
+#define GPS_RX_PIN 4
+#define GPS_TX_PIN 5
+
+#define I2C_SDA 10
+#define I2C_SCL 8
+
+#define PIN_BUZZER 11
+
+#define HAS_NEOPIXEL                         // Enable the use of neopixels
+#define NEOPIXEL_COUNT 1                     // How many neopixels are connected
+#define NEOPIXEL_DATA 2                      // gpio pin used to send data to the neopixels
+#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use
+#define ENABLE_AMBIENTLIGHTING               // Turn on Ambient Lighting
+
+// #define BUTTON_PIN 9
+#define BUTTON_EXTENDER
+
+#undef LORA_SCK
+#undef LORA_MISO
+#undef LORA_MOSI
+#undef LORA_CS
+
+// WaveShare Core1262-868M OK
+// https://www.waveshare.com/wiki/Core1262-868M
+#define USE_SX1262
+
+#define LORA_MISO 22
+#define LORA_SCK 20
+#define LORA_MOSI 21
+#define LORA_CS 23
+#define LORA_RESET RADIOLIB_NC
+#define LORA_DIO1 7
+#define LORA_BUSY 19
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_BUSY
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 3.0
+
+#define USE_SPISSD1306
+#ifdef USE_SPISSD1306
+#define SSD1306_NSS 6 // CS
+#define SSD1306_RS 18 // DC
+#define SSD1306_RESET 15
+// #define OLED_DG 1
+#endif
+#define SCREEN_TRANSITION_FRAMERATE 10
+#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini
index 59bc26000..065f22538 100644
--- a/variants/esp32s3/elecrow_panel/platformio.ini
+++ b/variants/esp32s3/elecrow_panel/platformio.ini
@@ -19,8 +19,6 @@ build_flags = ${esp32s3_base.build_flags} -Os
   -D MESHTASTIC_EXCLUDE_SERIAL=1
   -D MESHTASTIC_EXCLUDE_SOCKETAPI=1
   -D MESHTASTIC_EXCLUDE_SCREEN=1
-  -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-  -D HAS_TELEMETRY=0
   -D CONFIG_DISABLE_HAL_LOCKS=1
   -D USE_PIN_BUZZER
   -D HAS_SCREEN=0
diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini
index d5847959b..b47d5733f 100644
--- a/variants/esp32s3/picomputer-s3/platformio.ini
+++ b/variants/esp32s3/picomputer-s3/platformio.ini
@@ -2,7 +2,7 @@
 extends = esp32s3_base
 board = bpi_picow_esp32_s3
 board_check = true
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 ;OpenOCD flash method
 ;upload_protocol = esp-builtin
 ;Normal method
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h
new file mode 100644
index 000000000..15a26e991
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h
@@ -0,0 +1,28 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include "variant.h"
+#include 
+
+#define USB_VID 0x303a
+#define USB_PID 0x1001
+
+// The default Wire will be mapped to PMU and RTC
+static const uint8_t SDA = 9;
+static const uint8_t SCL = 40;
+
+// Default SPI will be mapped to Radio
+static const uint8_t SS = 12;
+static const uint8_t MOSI = 11;
+static const uint8_t MISO = 10;
+static const uint8_t SCK = 13;
+
+#define SPI_MOSI (11)
+#define SPI_SCK (13)
+#define SPI_MISO (10)
+#define SPI_CS (12)
+
+// LEDs
+#define LED_BUILTIN LED_GREEN
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
new file mode 100644
index 000000000..de4714efa
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
@@ -0,0 +1,88 @@
+; rak_wismeshtap2 rak3112
+
+[rak_wismeshtap_s3]
+extends = esp32s3_base
+board = wiscore_rak3312
+board_check = true
+upload_protocol = esptool
+board_build.partitions = default_8MB.csv
+
+build_flags = 
+  ${esp32_base.build_flags} 
+  -D RAK3312
+  -D RAK_WISMESH_TAP_V2 
+  -I variants/esp32s3/rak_wismesh_tap_v2
+
+lib_deps =
+  ${esp32s3_base.lib_deps}
+  lovyan03/LovyanGFX@^1.2.0
+
+[ft5x06]
+extends = mesh_tab_base
+build_flags = 
+  -D LGFX_TOUCH=FT5x06
+  -D LGFX_TOUCH_I2C_FREQ=100000
+  -D LGFX_TOUCH_I2C_PORT=0
+  -D LGFX_TOUCH_I2C_ADDR=0x38
+  -D LGFX_TOUCH_I2C_SDA=9
+  -D LGFX_TOUCH_I2C_SCL=40
+  -D LGFX_TOUCH_RST=-1
+  -D LGFX_TOUCH_INT=39
+
+[env:rak_wismesh_tap_v2-tft]
+extends = rak_wismeshtap_s3
+
+build_flags =
+  ${rak_wismeshtap_s3.build_flags}
+  -D CONFIG_ARDUHAL_ESP_LOG
+  -D CONFIG_ARDUHAL_LOG_COLORS=1
+  -D CONFIG_DISABLE_HAL_LOCKS=1
+  -D LV_LVGL_H_INCLUDE_SIMPLE
+  -D LV_CONF_INCLUDE_SIMPLE
+  -D LV_COMP_CONF_INCLUDE_SIMPLE
+  -D LV_USE_SYSMON=0
+  -D LV_USE_PROFILER=0
+  -D LV_USE_PERF_MONITOR=0
+  -D LV_USE_MEM_MONITOR=0
+  -D LV_USE_LOG=0
+  -D LV_BUILD_TEST=0
+  -D USE_LOG_DEBUG
+  -D LOG_DEBUG_INC=\"DebugConfiguration.h\"
+  -D RADIOLIB_SPI_PARANOID=0
+  -D INPUTDRIVER_BUTTON_TYPE=0
+  -D HAS_SDCARD
+  -D HAS_SCREEN=0
+  -D HAS_TFT=1
+  -D USE_PIN_BUZZER=PIN_BUZZER
+  -D RAM_SIZE=5120
+  -D LGFX_DRIVER_TEMPLATE
+  -D LGFX_DRIVER=LGFX_GENERIC
+  -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\"
+  -D LGFX_PIN_SCK=13
+  -D LGFX_PIN_MOSI=11
+  -D LGFX_PIN_MISO=10
+  -D LGFX_PIN_DC=42
+  -D LGFX_PIN_CS=12
+  -D LGFX_PIN_RST=-1
+  -D LGFX_PIN_BL=41
+  -D VIEW_320x240
+  -D USE_PACKET_API
+  ${ft5x06.build_flags}
+  -D LGFX_SCREEN_WIDTH=240
+  -D LGFX_SCREEN_HEIGHT=320
+  -D DISPLAY_SIZE=320x240 ; landscape mode
+  -D LGFX_PANEL=ST7789
+  -D LGFX_ROTATION=1
+  -D LGFX_TOUCH_X_MIN=0
+  -D LGFX_TOUCH_X_MAX=239
+  -D LGFX_TOUCH_Y_MIN=0
+  -D LGFX_TOUCH_Y_MAX=319
+  -D LGFX_TOUCH_ROTATION=2
+  -D LGFX_CFG_HOST=SPI3_HOST
+  -D MAP_FULL_REDRAW=1
+
+lib_deps =
+  ${rak_wismeshtap_s3.lib_deps}
+  ${device-ui_base.lib_deps}
+
+
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
new file mode 100644
index 000000000..2fc056557
--- /dev/null
+++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h
@@ -0,0 +1,71 @@
+#ifndef _VARIANT_RAK_WISMESHTAP_V2_H
+#define _VARIANT_RAK_WISMESHTAP_V2_H
+
+#define I2C_SDA 9
+#define I2C_SCL 40
+
+#define USE_SX1262
+
+#define LORA_SCK 5
+#define LORA_MISO 3
+#define LORA_MOSI 6
+#define LORA_CS 7
+#define LORA_RESET 8
+
+#ifdef USE_SX1262
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 47
+#define SX126X_BUSY 48
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#endif
+
+#define SX126X_POWER_EN (4)
+
+#define PIN_POWER_EN PIN_3V3_EN
+#define PIN_3V3_EN (14)
+
+#define LED_GREEN 46
+#define LED_BLUE 45
+
+#define PIN_LED1 LED_GREEN
+#define PIN_LED2 LED_BLUE
+
+#define LED_CONN LED_BLUE
+#define LED_PIN LED_GREEN
+#define ledOff(pin) pinMode(pin, INPUT)
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+#define HAS_GPS 1
+#define GPS_TX_PIN 43
+#define GPS_RX_PIN 44
+
+#define SPI_MOSI (11)
+#define SPI_SCK (13)
+#define SPI_MISO (10)
+#define SPI_CS (12)
+
+#define HAS_BUTTON 1
+#define BUTTON_PIN 0
+
+#define CANNED_MESSAGE_MODULE_ENABLE 1
+#define USE_VIRTUAL_KEYBOARD 1
+
+#define BATTERY_PIN 1
+#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
+#define ADC_MULTIPLIER 1.667
+
+#define PIN_BUZZER 38
+
+#define HAS_SDCARD 1
+#define SDCARD_USE_SPI1 1
+#define SDCARD_CS 2
+
+#define SPI_FREQUENCY 40000000
+#define SPI_READ_FREQUENCY 16000000
+
+#define SD_SPI_FREQUENCY 50000000
+
+#endif
\ No newline at end of file
diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
index f408054cf..25ec3ebfc 100644
--- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
+++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini
@@ -6,7 +6,7 @@ platform_packages =
 
 board = seeed-sensecap-indicator
 board_check = true
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 upload_protocol = esptool
 
 build_flags = ${esp32_base.build_flags}
diff --git a/variants/esp32s3/tlora-pager/pins_arduino.h b/variants/esp32s3/tlora-pager/pins_arduino.h
new file mode 100644
index 000000000..a6321f510
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/pins_arduino.h
@@ -0,0 +1,19 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include 
+
+#define USB_VID 0x303a
+#define USB_PID 0x1001
+
+// used for keyboard, battery gauge, charger and haptic driver
+static const uint8_t SDA = 3;
+static const uint8_t SCL = 2;
+
+// Default SPI will be mapped to Radio
+static const uint8_t SS = 36;
+static const uint8_t MOSI = 34;
+static const uint8_t MISO = 33;
+static const uint8_t SCK = 35;
+
+#endif /* Pins_Arduino_h */
diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini
new file mode 100644
index 000000000..312d46259
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/platformio.ini
@@ -0,0 +1,71 @@
+; LilyGo T-Lora-Pager
+[env:tlora-pager]
+extends = esp32s3_base
+board = t-deck-pro ; same as T-Deck Pro
+board_check = true
+board_build.partitions = default_16MB.csv
+upload_protocol = esptool
+
+build_flags = ${esp32s3_base.build_flags} 
+  -I variants/esp32s3/tlora-pager
+  -D T_LORA_PAGER 
+  -D BOARD_HAS_PSRAM
+  -D GPS_POWER_TOGGLE
+  -D HAS_SDCARD
+  -D SDCARD_USE_SPI1
+  -D ENABLE_ROTARY_PULLUP
+  -D ENABLE_BUTTON_PULLUP
+  -D HALF_STEP
+
+lib_deps = ${esp32s3_base.lib_deps}
+  lovyan03/LovyanGFX@1.2.7
+  earlephilhower/ESP8266Audio@1.9.9
+  earlephilhower/ESP8266SAM@1.0.1
+  adafruit/Adafruit DRV2605 Library@1.2.4
+  lewisxhe/PCF8563_Library@1.0.1
+  lewisxhe/SensorLib@0.3.1
+  https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip
+  https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip
+  https://github.com/mverch67/RotaryEncoder/archive/25a59d5745a6645536f921427d80b08e78f886d4.zip
+
+[env:tlora-pager-tft]
+board_level = extra
+extends = env:tlora-pager
+build_flags =
+  ${env:tlora-pager.build_flags}
+  -D CONFIG_DISABLE_HAL_LOCKS=1
+  -D INPUTDRIVER_ROTARY_TYPE=1
+  -D INPUTDRIVER_ROTARY_UP=40
+  -D INPUTDRIVER_ROTARY_DOWN=41
+  -D INPUTDRIVER_ROTARY_BTN=7
+  -D INPUTDRIVER_BUTTON_TYPE=0
+  -D HAS_SCREEN=1
+  -D HAS_TFT=1
+  -D USE_I2S_BUZZER
+  -D RAM_SIZE=5120
+  -D LV_LVGL_H_INCLUDE_SIMPLE
+  -D LV_CONF_INCLUDE_SIMPLE
+  -D LV_COMP_CONF_INCLUDE_SIMPLE
+  -D LV_USE_SYSMON=0
+  -D LV_USE_PROFILER=0
+  -D LV_USE_PERF_MONITOR=0
+  -D LV_USE_MEM_MONITOR=0
+  -D LV_USE_LOG=0
+  -D USE_LOG_DEBUG
+  -D LOG_DEBUG_INC=\"DebugConfiguration.h\"
+  -D RADIOLIB_SPI_PARANOID=0
+  -D LGFX_SCREEN_WIDTH=222
+  -D LGFX_SCREEN_HEIGHT=480
+  -D DISPLAY_SIZE=480x222 ; landscape mode
+  -D DISPLAY_SET_RESOLUTION
+  -D LGFX_DRIVER=LGFX_TLORA_PAGER
+  -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_LORA_PAGER.h\"
+;  -D LVGL_DRIVER=LVGL_T_LORA_PAGER
+;  -D LV_USE_ST7796=1
+  -D VIEW_480x222
+  -D USE_PACKET_API
+  -D MAP_FULL_REDRAW
+
+lib_deps =
+  ${env:tlora-pager.lib_deps}
+  ${device-ui_base.lib_deps}
diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h
new file mode 100644
index 000000000..337346ec5
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/rfswitch.h
@@ -0,0 +1,15 @@
+#include "RadioLib.h"
+
+static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[] = {
+    // mode                  DIO5  DIO6
+    {LR11x0::MODE_STBY, {LOW, LOW}},
+    {LR11x0::MODE_RX, {LOW, HIGH}},
+    {LR11x0::MODE_TX, {HIGH, LOW}},
+    {LR11x0::MODE_TX_HP, {HIGH, LOW}},
+    {LR11x0::MODE_TX_HF, {LOW, LOW}},
+    {LR11x0::MODE_GNSS, {LOW, LOW}},
+    {LR11x0::MODE_WIFI, {LOW, LOW}},
+    END_OF_MODE_TABLE,
+};
\ No newline at end of file
diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h
new file mode 100644
index 000000000..2875f6804
--- /dev/null
+++ b/variants/esp32s3/tlora-pager/variant.h
@@ -0,0 +1,142 @@
+// ST7796 TFT LCD
+#define TFT_CS 38
+#define ST7796_CS TFT_CS
+#define ST7796_RS 37    // DC
+#define ST7796_SDA MOSI // MOSI
+#define ST7796_SCK SCK
+#define ST7796_RESET -1
+#define ST7796_MISO MISO
+#define ST7796_BUSY -1
+#define ST7796_BL 42
+#define ST7796_SPI_HOST SPI2_HOST
+#define TFT_BL 42
+#define SPI_FREQUENCY 75000000
+#define SPI_READ_FREQUENCY 16000000
+#define TFT_HEIGHT 480
+#define TFT_WIDTH 222
+#define TFT_OFFSET_X 49
+#define TFT_OFFSET_Y 0
+#define TFT_OFFSET_ROTATION 3
+#define SCREEN_ROTATE
+#define SCREEN_TRANSITION_FRAMERATE 5
+#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness
+
+#define I2C_SDA SDA
+#define I2C_SCL SCL
+
+#define USE_POWERSAVE
+#define SLEEP_TIME 120
+
+// GNNS
+#define HAS_GPS 1
+#define GPS_BAUDRATE 38400
+#define GPS_RX_PIN 4
+#define GPS_TX_PIN 12
+#define PIN_GPS_PPS 13
+
+// PCF8563 RTC Module
+#if __has_include("pcf8563.h")
+#include "pcf8563.h"
+#endif
+#define PCF8563_RTC 0x51
+#define HAS_RTC 1
+
+// Rotary
+#define ROTARY_A (40)
+#define ROTARY_B (41)
+#define ROTARY_PRESS (7)
+
+#define BUTTON_PIN 0
+
+// SPI interface SD card slot
+#define SPI_MOSI MOSI
+#define SPI_SCK SCK
+#define SPI_MISO MISO
+#define SPI_CS 21
+#define SDCARD_CS SPI_CS
+#define SD_SPI_FREQUENCY 75000000U
+
+// TCA8418 keyboard
+#define I2C_NO_RESCAN
+#define KB_BL_PIN 46
+#define KB_INT 6
+#define CANNED_MESSAGE_MODULE_ENABLE 1
+
+// audio codec ES8311
+#define HAS_I2S
+#define DAC_I2S_BCK 11
+#define DAC_I2S_WS 18
+#define DAC_I2S_DOUT 45
+#define DAC_I2S_DIN 17
+#define DAC_I2S_MCLK 10
+
+// gyroscope BHI260AP
+#define HAS_BHI260AP
+
+// battery charger BQ25896
+#define HAS_PPM 1
+#define XPOWERS_CHIP_BQ25896
+
+// battery quality management BQ27220
+#define HAS_BQ27220 1
+#define BQ27220_I2C_SDA SDA
+#define BQ27220_I2C_SCL SCL
+#define BQ27220_DESIGN_CAPACITY 1500
+
+// NFC ST25R3916
+#define NFC_INT 5
+#define NFC_CS 39
+
+// External expansion chip XL9555
+#define USE_XL9555
+#define EXPANDS_DRV_EN (0)
+#define EXPANDS_AMP_EN (1)
+#define EXPANDS_KB_RST (2)
+#define EXPANDS_LORA_EN (3)
+#define EXPANDS_GPS_EN (4)
+#define EXPANDS_NFC_EN (5)
+#define EXPANDS_GPS_RST (7)
+#define EXPANDS_KB_EN (8)
+#define EXPANDS_GPIO_EN (9)
+#define EXPANDS_SD_DET (10)
+#define EXPANDS_SD_PULLEN (11)
+#define EXPANDS_SD_EN (12)
+
+// LoRa
+#define USE_SX1262
+#define USE_SX1268
+#define USE_SX1280
+#define USE_LR1121
+
+#define LORA_SCK 35
+#define LORA_MISO 33
+#define LORA_MOSI 34
+#define LORA_CS 36
+#define LORA_RESET 47
+
+#define LORA_DIO0 -1 // a No connect on the SX1262 module
+#define LORA_DIO1 14 // SX1262 IRQ
+#define LORA_DIO2 48 // SX1262 BUSY
+#define LORA_DIO3    // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled
+
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_DIO2
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 3.0
+
+#define SX128X_CS LORA_CS
+#define SX128X_DIO1 LORA_DIO1
+#define SX128X_BUSY LORA_DIO2
+#define SX128X_RESET LORA_RESET
+
+#define LR1121_IRQ_PIN LORA_DIO1
+#define LR1121_NRESET_PIN LORA_RESET
+#define LR1121_BUSY_PIN LORA_DIO2
+#define LR1121_SPI_NSS_PIN LORA_CS
+#define LR1121_SPI_SCK_PIN LORA_SCK
+#define LR1121_SPI_MOSI_PIN LORA_MOSI
+#define LR1121_SPI_MISO_PIN LORA_MISO
+#define LR11X0_DIO3_TCXO_VOLTAGE 3.0
+#define LR11X0_DIO_AS_RF_SWITCH
diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini
index ecb1cbd67..f17a27e17 100644
--- a/variants/esp32s3/unphone/platformio.ini
+++ b/variants/esp32s3/unphone/platformio.ini
@@ -3,7 +3,7 @@
 [env:unphone]
 extends = esp32s3_base
 board = unphone
-board_build.partitions = default_8MB.csv
+board_build.partitions = partition-table-8MB.csv
 upload_speed = 921600
 monitor_speed = 115200
 monitor_filters = esp32_exception_decoder
@@ -20,6 +20,7 @@ build_flags =
   -D UNPHONE_LORA=0
   -D UNPHONE_FACTORY_MODE=0
   -D USE_SX127x
+  -D SDCARD_CS=43
 
 build_src_filter =
   ${esp32s3_base.build_src_filter}
@@ -32,6 +33,7 @@ lib_deps = ${esp32s3_base.lib_deps}
 
 
 [env:unphone-tft]
+board_level = extra
 extends = env:unphone
 build_flags =
   ${env:unphone.build_flags}
@@ -40,6 +42,7 @@ build_flags =
   -D HAS_SCREEN=1
   -D HAS_TFT=1
   -D HAS_SDCARD
+  -D SDCARD_CS=43
   -D DISPLAY_SET_RESOLUTION
   -D RAM_SIZE=6144
   -D LV_CACHE_DEF_SIZE=2097152
@@ -52,8 +55,6 @@ build_flags =
   -D LV_USE_PERF_MONITOR=0
   -D LV_USE_MEM_MONITOR=0
   -D LV_USE_LOG=0
-  -D USE_LOG_DEBUG
-  -D LOG_DEBUG_INC=\"DebugConfiguration.h\"
   -D LGFX_SCREEN_WIDTH=320
   -D LGFX_SCREEN_HEIGHT=480
   -D DISPLAY_SIZE=320x480 ; portrait mode
diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h
index e186b5740..366b49233 100644
--- a/variants/esp32s3/unphone/variant.h
+++ b/variants/esp32s3/unphone/variant.h
@@ -52,7 +52,6 @@
 #undef GPS_TX_PIN
 
 #define SD_SPI_FREQUENCY 25000000
-#define SDCARD_CS 43
 
 #define LED_PIN 13     // the red part of the RGB LED
 #define LED_STATE_ON 0 // State when LED is lit
diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h
index b7b39d6e8..11a6c0bd3 100644
--- a/variants/native/portduino-buildroot/variant.h
+++ b/variants/native/portduino-buildroot/variant.h
@@ -2,4 +2,4 @@
 #define CANNED_MESSAGE_MODULE_ENABLE 1
 #define HAS_GPS 1
 #define MAX_RX_TOPHONE settingsMap[maxtophone]
-#define MAX_NUM_NODES settingsMap[maxnodes]
\ No newline at end of file
+#define MAX_NUM_NODES settingsMap[maxnodes]
diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini
index 62942a80e..c47ab8bf1 100644
--- a/variants/native/portduino/platformio.ini
+++ b/variants/native/portduino/platformio.ini
@@ -3,7 +3,10 @@ extends = portduino_base
 build_flags = ${portduino_base.build_flags} -I variants/native/portduino
   -I /usr/include
 board = cross_platform
-lib_deps = ${portduino_base.lib_deps}
+lib_deps = 
+  ${portduino_base.lib_deps}
+  melopero/Melopero RV3028@^1.1.0
+
 build_src_filter = ${portduino_base.build_src_filter}
 
 [env:native]
diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h
index ce7dbd865..a7ca865be 100644
--- a/variants/native/portduino/variant.h
+++ b/variants/native/portduino/variant.h
@@ -4,4 +4,7 @@
 #define CANNED_MESSAGE_MODULE_ENABLE 1
 #define HAS_GPS 1
 #define MAX_RX_TOPHONE settingsMap[maxtophone]
-#define MAX_NUM_NODES settingsMap[maxnodes]
\ No newline at end of file
+#define MAX_NUM_NODES settingsMap[maxnodes]
+
+// RAK12002 RTC Module
+#define RV3028_RTC (uint8_t)0b1010010
\ No newline at end of file
diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
index 72ac6320d..5c1047aae 100644
--- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
+++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini
@@ -1,6 +1,7 @@
 ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921
 [env:gat562_mesh_trial_tracker]
 extends = nrf52840_base
+board_level = extra
 board = gat562_mesh_trial_tracker
 board_check = true
 build_flags = ${nrf52840_base.build_flags}
diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h
index f4f0baf13..7e82733aa 100644
--- a/variants/nrf52840/heltec_mesh_node_t114/variant.h
+++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h
@@ -208,7 +208,7 @@ No longer populated on PCB
 #undef AREF_VOLTAGE
 #define AREF_VOLTAGE 3.0
 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0
-#define ADC_MULTIPLIER (4.90F)
+#define ADC_MULTIPLIER (4.916F)
 
 #define HAS_RTC 0
 #ifdef __cplusplus
diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini
new file mode 100644
index 000000000..65d26dc40
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini
@@ -0,0 +1,19 @@
+; First prototype nrf52840/sx1262 device
+[env:heltec-mesh-solar]
+extends = nrf52840_base
+board = heltec_mesh_solar
+board_level = pr
+debug_tool = jlink
+
+# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling.
+build_flags = ${nrf52840_base.build_flags}
+  -Ivariants/nrf52840/heltec_mesh_solar
+  -DGPS_POWER_TOGGLE
+  -DHELTEC_MESH_SOLAR
+
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar>
+lib_deps = 
+  ${nrf52840_base.lib_deps}
+  https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip
+  lewisxhe/PCF8563_Library@^1.0.1
+  ArduinoJson@6.21.4
diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp
new file mode 100644
index 000000000..8236d7cf4
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp
@@ -0,0 +1,36 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "nrf.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+    // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
+    0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+
+    // P1
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+
+void initVariant()
+{
+  pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT);
+}
diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h
new file mode 100644
index 000000000..33c2b2556
--- /dev/null
+++ b/variants/nrf52840/heltec_mesh_solar/variant.h
@@ -0,0 +1,157 @@
+/*
+ Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+ Copyright (c) 2016 Sandeep Mistry All right reserved.
+ Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ See the GNU Lesser General Public License for more details.
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_HELTEC_NRF_
+#define _VARIANT_HELTEC_NRF_
+/** Master clock frequency */
+#define VARIANT_MCK (64000000ul)
+
+#define USE_LFXO // Board uses 32khz crystal for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (1)
+#define NUM_ANALOG_OUTPUTS (0)
+
+
+#define PIN_LED1 (0 + 12) // green (confirmed on 1.0 board)
+#define LED_BLUE PIN_LED1 // fake for bluefruit library
+#define LED_GREEN PIN_LED1
+#define LED_BUILTIN LED_GREEN
+#define LED_STATE_ON 0  // State when LED is lit
+
+#define HAS_NEOPIXEL                         // Enable the use of neopixels
+#define NEOPIXEL_COUNT 1                     // How many neopixels are connected
+#define NEOPIXEL_DATA (32+15)                // gpio pin used to send data to the neopixels
+#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use
+
+/*
+ * Buttons
+ */
+#define PIN_BUTTON1 (32 + 10)
+// #define PIN_BUTTON2 (0 + 18)      // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular
+// GPIO
+
+/*
+No longer populated on PCB
+*/
+#define PIN_SERIAL2_RX (0 + 9)
+#define PIN_SERIAL2_TX (0 + 10)
+//  #define PIN_SERIAL2_EN (0 + 17)
+
+/*
+ * I2C
+ */
+
+#define WIRE_INTERFACES_COUNT 2
+
+// I2C bus 0
+// Routed to footprint for PCF8563TS RTC
+// Not populated on T114 V1, maybe in future?
+#define PIN_WIRE_SDA (0 + 6) // P0.26
+#define PIN_WIRE_SCL (0 + 26) // P0.26
+
+// I2C bus 1
+// Available on header pins, for general use
+#define PIN_WIRE1_SDA (0 + 30) // P0.30
+#define PIN_WIRE1_SCL (0 + 5) // P0.13
+
+/*
+ * Lora radio
+ */
+
+#define USE_SX1262
+// #define USE_SX1268
+#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead
+#define LORA_CS  (0 + 24)
+#define SX126X_DIO1 (0 + 20)
+// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching
+// #define SX1262_DIO3 (0 + 21)
+// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the
+//    main
+// CPU?
+#define SX126X_BUSY  (0 + 17)
+#define SX126X_RESET (0 + 25)
+// Not really an E22 but TTGO seems to be trying to clone that
+#define SX126X_DIO2_AS_RF_SWITCH
+#define SX126X_DIO3_TCXO_VOLTAGE 1.8
+
+/*
+ * GPS pins
+ */
+
+#define GPS_L76K
+
+// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K
+// #define GPS_RESET_MODE LOW
+// #define PIN_GPS_EN (21)
+#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
+#define VEXT_ON_VALUE HIGH
+// #define GPS_EN_ACTIVE HIGH
+#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake
+#define PIN_GPS_PPS (32 + 4)
+// Seems to be missing on this new board
+// #define PIN_GPS_PPS (32 + 4)  // Pulse per second input from the GPS
+#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU
+#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS
+
+#define GPS_THREAD_INTERVAL 50
+
+#define PIN_SERIAL1_RX GPS_TX_PIN
+#define PIN_SERIAL1_TX GPS_RX_PIN
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 1
+
+// For LORA, spi 0
+#define PIN_SPI_MISO (0 + 23)
+#define PIN_SPI_MOSI (0 + 22)
+#define PIN_SPI_SCK  (0 + 19)
+
+// #define PIN_PWR_EN (0 + 6)
+
+// To debug via the segger JLINK console rather than the CDC-ACM serial device
+// #define USE_SEGGER
+
+#define BQ4050_SDA_PIN                      (32+1) // I2C data line pin
+#define BQ4050_SCL_PIN                      (32+0) // I2C clock line pin
+#define BQ4050_EMERGENCY_SHUTDOWN_PIN       (32+3) // Emergency shutdown pin
+
+#define HAS_RTC 0
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif
diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini
index 8216a704a..466362242 100644
--- a/variants/nrf52840/meshlink/platformio.ini
+++ b/variants/nrf52840/meshlink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink
diff --git a/variants/nrf52840/meshlink_eink/platformio.ini b/variants/nrf52840/meshlink_eink/platformio.ini
index a48a9e695..af5a0040e 100644
--- a/variants/nrf52840/meshlink_eink/platformio.ini
+++ b/variants/nrf52840/meshlink_eink/platformio.ini
@@ -4,6 +4,7 @@
 [env:meshlink_eink]
 extends = nrf52840_base
 board = meshlink
+board_level = extra
 ;board_check = true
 build_flags = ${nrf52840_base.build_flags}
   -I variants/nrf52840/meshlink_eink
diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini
index 83feaa06c..6bf5f44cb 100644
--- a/variants/nrf52840/rak4631/platformio.ini
+++ b/variants/nrf52840/rak4631/platformio.ini
@@ -22,6 +22,7 @@ lib_deps =
   https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
   rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
   beegee-tokyo/RAK12035_SoilMoisture@^1.0.4
+  # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
   https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
 
 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini
index 79cdb28c7..4be8843a2 100644
--- a/variants/nrf52840/rak4631_eth_gw/platformio.ini
+++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini
@@ -31,7 +31,8 @@ lib_deps =
   melopero/Melopero RV3028@^1.1.0
   https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
   rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
-  https://github.com/meshtastic/RAK12034-BMX160/archive/4821355fb10390ba8557dc43ca29a023bcfbb9d9.zip
+  # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
+  https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
   bblanchon/ArduinoJson @ 6.21.4
 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
 ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
index a32753343..7fb890303 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h
@@ -18,16 +18,9 @@
 
 // Shared NicheGraphics components
 // --------------------------------
-#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h"
-#include "graphics/niche/Drivers/EInk/GDEY0213B74.h"
+#include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h"
 #include "graphics/niche/Inputs/TwoButton.h"
 
-// Special case - fix T-Echo's touch button
-// ----------------------------------------
-// On a handful of T-Echos, LoRa TX triggers the capacitive touch
-// To avoid this, we lockout the button during TX
-#include "mesh/RadioLibInterface.h"
-
 void setupNicheGraphics()
 {
     using namespace NicheGraphics;
@@ -41,7 +34,7 @@ void setupNicheGraphics()
     // E-Ink Driver
     // -----------------------------
 
-    Drivers::EInk *driver = new Drivers::GDEY0213B74;
+    Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN;
     driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES);
 
     // InkHUD
@@ -53,8 +46,7 @@ void setupNicheGraphics()
     inkhud->setDriver(driver);
 
     // Set how many FAST updates per FULL update
-    // Set how unhealthy additional FAST updates beyond this number are
-    inkhud->setDisplayResilience(7, 1.5);
+    inkhud->setDisplayResilience(15);
 
     // Select fonts
     InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252;
@@ -62,16 +54,10 @@ void setupNicheGraphics()
     InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252;
 
     // Customize default settings
-    inkhud->persistence->settings.userTiles.maxCount = 2;              // Two applets side-by-side
-                                                                       // 270 degrees clockwise
+    inkhud->persistence->settings.rotation = 1;                        // 90 degrees clockwise
     inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery
-    inkhud->persistence->settings.optionalMenuItems.backlight = true;  // Until proves capacitive button works by touching it
-    inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
-
-    // Setup backlight controller
-    // Note: AUX button attached further down
-    Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance();
-    backlight->setPin(PIN_EINK_EN);
+    inkhud->persistence->settings.userTiles.count = 1;    // One tile only by default, keep things simple for new users
+    inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side
 
     // Pick applets
     // Note: order of applets determines priority of "auto-show" feature
@@ -83,11 +69,9 @@ void setupNicheGraphics()
     inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet);            // -
     inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0);         // Activated, no autoshow, default on tile 0
 
-    inkhud->persistence->settings.rotation = 1;
-    // inkhud->persistence->printSettings(&inkhud->persistence->settings);
     //  Start running InkHUD
     inkhud->begin();
-    // inkhud->persistence->printSettings(&inkhud->persistence->settings);
+
     //  Buttons
     //  --------------------------
 
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
index 52ff39d49..7f9eb0e2c 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini
@@ -1,17 +1,47 @@
 [env:seeed_wio_tracker_L1_eink]
 board = seeed_wio_tracker_L1
-extends = nrf52840_base, inkhud
+extends = nrf52840_base
 ;board_level = extra
 build_flags = ${nrf52840_base.build_flags}
-  ${inkhud.build_flags}
   -I variants/nrf52840/seeed_wio_tracker_L1_eink
   -D SEEED_WIO_TRACKER_L1_EINK
   -D SEEED_WIO_TRACKER_L1
   -I src/platform/nrf52/softdevice
   -I src/platform/nrf52/softdevice/nrf52
+  -DUSE_EINK
+  -DEINK_DISPLAY_MODEL=GxEPD2_213_B74
+  -DEINK_WIDTH=250
+  -DEINK_HEIGHT=122
+  -DUSE_EINK_DYNAMICDISPLAY            ; Enable Dynamic EInk
+  -DEINK_LIMIT_FASTREFRESH=10          ; How many consecutive fast-refreshes are permitted
+  -DEINK_LIMIT_RATE_BACKGROUND_SEC=30  ; Minimum interval between BACKGROUND updates
+  -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1   ; Minimum interval between RESPONSIVE updates
+;   -D EINK_LIMIT_GHOSTING_PX=2000      ; (Optional) How much image ghosting is tolerated
+  -DEINK_BACKGROUND_USES_FAST          ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
+  -DEINK_HASQUIRK_GHOSTING             ; Display model is identified as "prone to ghosting"
+  -DEINK_HASQUIRK_WEAKFASTREFRESH      ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight
 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld
-build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter}
+build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink>
 lib_deps =
-  ${inkhud.lib_deps}
   ${nrf52840_base.lib_deps}
+  https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d
 debug_tool = jlink
+
+[env:seeed_wio_tracker_L1_eink-inkhud]
+board = seeed_wio_tracker_L1
+extends = nrf52840_base, inkhud
+build_flags = 
+  ${nrf52840_base.build_flags}
+  ${inkhud.build_flags}
+  -I variants/nrf52840/seeed_wio_tracker_L1_eink
+  -D SEEED_WIO_TRACKER_L1
+  -D BUTTON_PIN=D13
+board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld
+build_src_filter = 
+  ${nrf52_base.build_src_filter}
+  ${inkhud.build_src_filter}
+  +<../variants/nrf52840/seeed_wio_tracker_L1_eink>
+lib_deps =
+  ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space)
+  ${nrf52840_base.lib_deps}
+debug_tool = jlink
\ No newline at end of file
diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
index 98a7b2c39..f33d200b1 100644
--- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
+++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h
@@ -33,17 +33,10 @@
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Button Configuration
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-
-#ifdef BUTTON_PIN
-#undef BUTTON_PIN
-#endif
-
-#define BUTTON_PIN D13 // This is the Program Button
+#define CANCEL_BUTTON_PIN D13 // This is the Program Button
 // #define BUTTON_NEED_PULLUP   1
-#define BUTTON_ACTIVE_LOW true
-#define BUTTON_ACTIVE_PULLUP false
-
-#define BUTTON_PIN_TOUCH 13 // Touch button
+#define CANCEL_BUTTON_ACTIVE_LOW true
+#define CANCEL_BUTTON_ACTIVE_PULLUP false
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Digital Pin Mapping (D0-D10)
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -116,7 +109,7 @@ static const uint8_t SCL = PIN_WIRE_SCL;
 #define PIN_EINK_SCLK 31
 #define PIN_EINK_MOSI 33
 #define PIN_EINK_EN 14   // unused
-#define PIN_SPI1_MISO 15 // unused
+#define PIN_SPI1_MISO -1 // 15 unused
 #define PIN_SPI1_MOSI PIN_EINK_MOSI
 #define PIN_SPI1_SCK PIN_EINK_SCLK
 
@@ -175,7 +168,17 @@ static const uint8_t SCL = PIN_WIRE_SCL;
 //  joystick
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
+// trackball
+#define HAS_TRACKBALL 1
+#define TB_UP 25
+#define TB_DOWN 26
+#define TB_LEFT 27
+#define TB_RIGHT 28
+#define TB_PRESS 29
+#define TB_DIRECTION FALLING
+
 #define CANNED_MESSAGE_MODULE_ENABLE 1
+#define CANNED_MESSAGE_ADD_CONFIRMATION 1
 
 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 //  Compatibility Definitions
diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h
index 81b4ef3fb..403552ec0 100644
--- a/variants/nrf52840/tracker-t1000-e/variant.h
+++ b/variants/nrf52840/tracker-t1000-e/variant.h
@@ -124,8 +124,7 @@ extern "C" {
 #define GPS_RTC_INT (0 + 15)     // P0.15, normal is LOW, wake by HIGH
 #define GPS_RESETB_OUT (32 + 14) // P1.14, always input pull_up
 
-#define GPS_FIX_HOLD_TIME 15000 // ms
-#define BATTERY_PIN 2           // P0.02/AIN0, BAT_ADC
+#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC
 #define BATTERY_IMMUTABLE
 #define ADC_MULTIPLIER (2.0F)
 // P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE
diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h
index eb6a34d6c..02f8a20b2 100644
--- a/variants/nrf52840/wio-t1000-s/variant.h
+++ b/variants/nrf52840/wio-t1000-s/variant.h
@@ -123,7 +123,6 @@ extern "C" {
 #define GPS_RESETB_OUT (32 + 14) // P1.14, awlays input pull_up
 
 // #define GPS_THREAD_INTERVAL 50
-#define GPS_FIX_HOLD_TIME 15000 // ms
 
 #define BATTERY_PIN 2
 // #define ADC_CHANNEL ADC1_GPIO2_CHANNEL
@@ -157,4 +156,4 @@ extern "C" {
  *        Arduino objects - C++ only
  *----------------------------------------------------------------------------*/
 
-#endif // _VARIANT_WIO_SDK_WM1110_
\ No newline at end of file
+#endif // _VARIANT_WIO_SDK_WM1110_
diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h
new file mode 100644
index 000000000..ac472c07e
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#define PINS_COUNT (25u)
+#define NUM_DIGITAL_PINS (25u)
+#define NUM_ANALOG_INPUTS (4u)
+#define NUM_ANALOG_OUTPUTS (0u)
+#define ADC_RESOLUTION (12u)
+
+// LEDs
+#define PIN_LED (24u)
+
+// Serial
+#define PIN_SERIAL1_TX (16u)
+#define PIN_SERIAL1_RX (17u)
+
+// SPI
+#define PIN_SPI0_MISO (20u)
+#define PIN_SPI0_MOSI (23u)
+#define PIN_SPI0_SCK (22u)
+#define PIN_SPI0_SS (21u)
+
+// Connected to LoRa module
+#define PIN_SPI1_MISO (12u)
+#define PIN_SPI1_MOSI (11u)
+#define PIN_SPI1_SCK (10u)
+#define PIN_SPI1_SS (9u)
+#define RFM95W_SS (9u)
+#define RFM95W_DIO0 (14u)
+#define RFM95W_DIO1 (15u)
+#define RFM95W_DIO2 (18u)
+#define RFM95W_RST (13u)
+#define RFM95W_SPI SPI1
+
+// Wire
+#define PIN_WIRE0_SDA (0u)
+#define PIN_WIRE0_SCL (1u)
+
+// Not pinned out
+#define PIN_WIRE1_SDA (31u)
+#define PIN_WIRE1_SCL (31u)
+#define PIN_SERIAL2_RX (31u)
+#define PIN_SERIAL2_TX (31u)
+
+#define SERIAL_HOWMANY (1u)
+#define SPI_HOWMANY (2u)
+#define WIRE_HOWMANY (1u)
+
+#define LED_BUILTIN PIN_LED
+
+static const uint8_t D0 = (16u);
+static const uint8_t D1 = (17u);
+static const uint8_t D2 = (20u);
+static const uint8_t D3 = (23u);
+static const uint8_t D4 = (22u);
+static const uint8_t D5 = (2u);
+static const uint8_t D6 = (3u);
+static const uint8_t D7 = (0u);
+static const uint8_t D8 = (1u);
+static const uint8_t D9 = (4u);
+static const uint8_t D10 = (5u);
+static const uint8_t D11 = (6u);
+static const uint8_t D12 = (7u);
+static const uint8_t D13 = (8u);
+static const uint8_t D14 = (13u);
+static const uint8_t D15 = (14u);
+static const uint8_t D16 = (15u);
+static const uint8_t D17 = (18u);
+static const uint8_t D18 = (24u);
+
+static const uint8_t A0 = (26u);
+static const uint8_t A1 = (27u);
+static const uint8_t A2 = (28u);
+static const uint8_t A3 = (29u);
+static const uint8_t A4 = (19u);
+static const uint8_t A5 = (21u);
+
+#ifndef SS
+#define SS PIN_SPI1_SS
+#endif
\ No newline at end of file
diff --git a/variants/rp2040/challenger_2040_lora/platformio.ini b/variants/rp2040/challenger_2040_lora/platformio.ini
new file mode 100644
index 000000000..4a709d650
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/platformio.ini
@@ -0,0 +1,16 @@
+[env:challenger_2040_lora]
+extends = rp2040_base
+board = challenger_2040_lora
+board_level = extra
+upload_protocol = picotool
+# add our variants files to the include and src paths
+build_flags = 
+  ${rp2040_base.build_flags} 
+  -D PRIVATE_HW
+  -I variants/rp2040/challenger_2040_lora
+  -D DEBUG_RP2040_PORT=Serial
+  -D HW_SPI1_DEVICE
+lib_deps =
+  ${rp2040_base.lib_deps}
+debug_build_flags = ${rp2040_base.build_flags}
+debug_tool = cmsis-dap ; for e.g. Picotool
diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h
new file mode 100644
index 000000000..552f90720
--- /dev/null
+++ b/variants/rp2040/challenger_2040_lora/variant.h
@@ -0,0 +1,39 @@
+// Define SS for compatibility with libraries expecting a default SPI chip select pin
+
+#define ARDUINO_ARCH_AVR
+
+#define EXT_NOTIFY_OUT 0xFFFFFFFF
+#define BUTTON_PIN 0xFFFFFFFF
+
+#define LED_PIN PIN_LED
+
+#define USE_RF95 // RFM95/SX127x
+
+#undef LORA_SCK
+#undef LORA_MISO
+#undef LORA_MOSI
+#undef LORA_CS
+
+// https://gitlab.com/invectorlabs/hw/challenger_rp2040_lora
+#define LORA_SCK 10  // Clock
+#define LORA_CS 9    // Chip Select
+#define LORA_MOSI 11 // Serial Data Out
+#define LORA_MISO 12 // Serial Data In
+
+#define LORA_RESET 13 // Reset
+
+#define LORA_DIO0 14         // DIO0
+#define LORA_DIO1 15         // DIO1
+#define LORA_DIO2 18         // DIO2
+#define LORA_DIO3 0xFFFFFFFF // Not connected
+#define LORA_DIO4 0xFFFFFFFF // Not connected
+#define LORA_DIO5 0xFFFFFFFF // Not connected
+
+#ifdef USE_SX1262
+#define SX126X_CS LORA_CS
+#define SX126X_DIO1 LORA_DIO1
+#define SX126X_BUSY LORA_DIO2
+#define SX126X_RESET LORA_RESET
+#define SX126X_DIO2_AS_RF_SWITCH
+// #define SX126X_DIO3_TCXO_VOLTAGE 1.8
+#endif
\ No newline at end of file
diff --git a/variants/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini
index acf19d757..b70eff6d7 100644
--- a/variants/rp2040/ec_catsniffer/platformio.ini
+++ b/variants/rp2040/ec_catsniffer/platformio.ini
@@ -1,6 +1,7 @@
 [env:catsniffer]
 extends = rp2040_base
 board = rpipico
+board_level = extra
 upload_protocol = picotool
 build_flags = 
   ${rp2040_base.build_flags}
diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
index c011f62c9..290982405 100644
--- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
+++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini
@@ -12,7 +12,5 @@ build_flags =
   -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
   -DMESHTASTIC_EXCLUDE_I2C=1
   -DMESHTASTIC_EXCLUDE_GPS=1
-  ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF
-  ;-DCFG_DEBUG
 
 upload_port = stlink
\ No newline at end of file
diff --git a/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h
new file mode 100644
index 000000000..daf4aaaf9
--- /dev/null
+++ b/variants/stm32/CDEBYTE_E77-MBL/rfswitch.h
@@ -0,0 +1,9 @@
+// From E77-900M22S Product Specification
+// https://www.cdebyte.com/pdf-down.aspx?id=2963
+// Note 1: PA6 and PA7 pins are used as internal control RF switches of the module, PA6 = RF_TXEN, PA7 = RF_RXEN, RF_TXEN=1
+// RF_RXEN=0 is the transmit channel, and RF_TXEN=0 RF_RXEN=1 is the receiving channel
+
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA7, PA6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
\ No newline at end of file
diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h
index 52801dac7..317f44489 100644
--- a/variants/stm32/CDEBYTE_E77-MBL/variant.h
+++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h
@@ -18,5 +18,4 @@ Do not expect a working Meshtastic device with this target.
 #define LED_PIN PB4 // LED1
 // #define LED_PIN PB3 // LED2
 #define LED_STATE_ON 1
-
 #endif
diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini
index 4f9edbb92..7fc6c7cba 100644
--- a/variants/stm32/rak3172/platformio.ini
+++ b/variants/stm32/rak3172/platformio.ini
@@ -6,10 +6,14 @@ board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem
 build_flags =
   ${stm32_base.build_flags}
   -Ivariants/stm32/rak3172
+  -DRAK3172
+  -DENABLE_HWSERIAL1
+  -DPIN_SERIAL1_RX=PB7
+  -DPIN_SERIAL1_TX=PB6
   -DPIN_WIRE_SDA=PA11
   -DPIN_WIRE_SCL=PA12
   -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
   -DMESHTASTIC_EXCLUDE_I2C=1
   -DMESHTASTIC_EXCLUDE_GPS=1
-  ;-DCFG_DEBUG
+
 upload_port = stlink
diff --git a/variants/stm32/rak3172/rfswitch.h b/variants/stm32/rak3172/rfswitch.h
new file mode 100644
index 000000000..2dced3c7c
--- /dev/null
+++ b/variants/stm32/rak3172/rfswitch.h
@@ -0,0 +1,7 @@
+// Pins from https://forum.rakwireless.com/t/rak3172-internal-schematic/4557/2
+// PB8, PC13
+
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
\ No newline at end of file
diff --git a/variants/stm32/wio-e5/rfswitch.h b/variants/stm32/wio-e5/rfswitch.h
new file mode 100644
index 000000000..3eadd9b5c
--- /dev/null
+++ b/variants/stm32/wio-e5/rfswitch.h
@@ -0,0 +1,8 @@
+/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/
+ * Wio-E5 module ONLY transmits through RFO_HP
+ * Receive: PA4=1, PA5=0
+ * Transmit(high output power, SMPS mode): PA4=0, PA5=1 */
+static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
+
+static const Module::RfSwitchMode_t rfswitch_table[4] = {
+    {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
diff --git a/version.properties b/version.properties
index f9e2cb279..cbf8265d9 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
 [VERSION]  
 major = 2
 minor = 7
-build = 6
+build = 9