diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 5c1c453dd..350ca290c 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -23,7 +23,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 ed14907dc..6ff12221b 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -43,11 +43,15 @@ 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 - run: pip install -U platformio + - name: Uncomment build epoch + shell: bash + run: | + sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini - name: Generate matrix id: jsonStep run: | @@ -370,7 +374,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -439,7 +443,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -494,7 +498,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 874715638..e10e20a04 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,12 +9,12 @@ plugins: lint: enabled: - checkov@3.2.469 - - renovate@41.93.2 + - renovate@41.94.0 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.65.0 + - trivy@0.66.0 - taplo@0.10.0 - ruff@0.12.11 - isort@6.0.1 @@ -23,7 +23,7 @@ lint: - 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.11.0 - black@25.1.0 diff --git a/bin/device-install.bat b/bin/device-install.bat index 24c841e4b..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=" @@ -17,7 +18,8 @@ SET "BPS_RESET=0" 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_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 @@ -119,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." @@ -163,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%. @@ -173,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. @@ -217,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" @@ -233,14 +250,14 @@ IF NOT EXIST !SPIFFS_FILENAME! CALL :LOG_MESSAGE ERROR "File does not exist: "!S @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash "!FILENAME!", but first erasing and writing system information..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase-flash || GOTO eof -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash 0x00 "!FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! erase_flash || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x00 "!FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash BLEOTA "!OTA_FILENAME!" at OTA_OFFSET !OTA_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !OTA_OFFSET! "!OTA_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Trying to flash SPIFFS "!SPIFFS_FILENAME!" at SPIFFS_OFFSET !SPIFFS_OFFSET!..." -CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof +CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash !SPIFFS_OFFSET! "!SPIFFS_FILENAME!" || GOTO eof CALL :LOG_MESSAGE INFO "Script complete!." @@ -252,9 +269,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-install.sh b/bin/device-install.sh index c2ba7539a..594f9dd6b 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -5,33 +5,39 @@ 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" @@ -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 @@ -202,11 +215,11 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then echo "Trying to flash ${FILENAME}, but first erasing and writing system information" $ESPTOOL_CMD erase-flash - $ESPTOOL_CMD write-flash 0x00 "${FILENAME}" + $ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write-flash $OTA_OFFSET "${OTAFILE}" + $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write-flash $OFFSET "${SPIFFSFILE}" + $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" else show_help diff --git a/bin/device-update.bat b/bin/device-update.bat index 9077ae5b9..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!." diff --git a/bin/device-update.sh b/bin/device-update.sh index 7f603e070..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/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/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 61880c709..16bb0eb96 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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/10f02441ec7dcd099c4c5165c709afc3e0e3cb88.zip + https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/protobufs b/protobufs index 4c4427c4a..a84657c22 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 +Subproject commit a84657c220421536f18d11fc5edf680efadbceeb diff --git a/src/Power.cpp b/src/Power.cpp index a123fe984..06c6a9089 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -833,16 +833,25 @@ 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(threadlist); 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)); lastheap = memGet.getFreeHeap(); @@ -856,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 diff --git a/src/configuration.h b/src/configuration.h index 81632c89e..d5adba028 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -431,6 +431,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_SERIAL 1 #define MESHTASTIC_EXCLUDE_POWERSTRESS 1 #define MESHTASTIC_EXCLUDE_ADMIN 1 +#define MESHTASTIC_EXCLUDE_AMBIENTLIGHTING 1 #endif // // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index b2904f2de..7a253ff50 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1546,10 +1546,9 @@ 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) == RTCSetResultSuccess) { - LOG_DEBUG("Time set."); + 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; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index e208e2df9..39b633e47 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -130,11 +130,15 @@ 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) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif @@ -252,11 +256,15 @@ 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) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 53fb8e993..3cf84f415 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -1,6 +1,7 @@ #include "graphics/SharedUIDisplay.h" #include "RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" #include "power.h" @@ -57,7 +58,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only) +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -73,7 +74,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int screenW = display->getWidth(); const int screenH = display->getHeight(); - if (!battery_only) { + if (!force_no_invert) { // === Inverted Header Background === if (isInverted) { display->setColor(BLACK); @@ -191,13 +192,28 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeX = screenW - xOffset - timeStrWidth + 4; - if (rtc_sec > 0 && !battery_only) { + if (rtc_sec > 0) { // === Build Time String === long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + // === Build Date String === + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char dateLine[40]; + + if (isHighResolution) { + snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); + } else { + if (hasUnreadMessage) { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); + } else { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); + } + } + if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; @@ -206,7 +222,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } - timeStrWidth = display->getStringWidth(timeStr); + if (show_date) { + timeStrWidth = display->getStringWidth(dateLine); + } else { + timeStrWidth = display->getStringWidth(timeStr); + } timeX = screenW - xOffset - timeStrWidth + 3; // === Show Mail or Mute Icon to the Left of Time === @@ -233,7 +253,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(BLACK); @@ -248,7 +268,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } else { int iconX = iconRightEdge - (mail_width - 2); int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->setColor(BLACK); @@ -291,10 +311,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } } - // === Draw Time === - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); + if (show_date) { + // === Draw Date === + display->drawString(timeX, textY, dateLine); + if (isBold) + display->drawString(timeX - 1, textY, dateLine); + } else { + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + } } else { // === No Time Available: Mail/Mute Icon Moves to Far Right === diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index b8d82795e..e1a7c6383 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -49,7 +49,8 @@ void determineResolution(int16_t screenheight, int16_t screenwidth); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, + bool show_date = false); const int *getTextPositions(OLEDDisplay *display); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 08acc3e55..3eeb17ef0 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1128,6 +1128,15 @@ 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) { diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 27672ad29..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); diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 6d3f13842..bb8cdd561 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -2,7 +2,6 @@ #if HAS_SCREEN #include "ClockRenderer.h" #include "NodeDB.h" -#include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" @@ -190,8 +189,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true); - int line = 0; + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -301,29 +299,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, secondString); #endif - - display->setFont(FONT_SMALL); - // Display GPS derived date - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char fullLine[40]; - xOffset = 1; - if (isHighResolution) { - snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); - } else { - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); - } - if (hasUnreadMessage) { - if (isHighResolution) { - xOffset = 23; - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); - } else { - xOffset = 15; - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[5]); - } - } - display->drawString(display->getWidth() - xOffset - display->getStringWidth(fullLine), getTextPositions(display)[line], - fullLine); } void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) @@ -338,8 +313,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true); - int line = 0; + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -537,29 +511,6 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // draw second hand display->drawLine(centerX, centerY, secondX, secondY); #endif - - display->setFont(FONT_SMALL); - // Display GPS derived date - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char fullLine[40]; - int xOffset = 1; - if (isHighResolution) { - snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); - } else { - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); - } - if (hasUnreadMessage) { - if (isHighResolution) { - xOffset = 23; - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); - } else { - xOffset = 15; - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[5]); - } - } - display->drawString(display->getWidth() - xOffset - display->getStringWidth(fullLine), getTextPositions(display)[line], - fullLine); } } diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 3c8e1dd9d..eada150f9 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -1,5 +1,6 @@ #pragma once +#include "NodeDB.h" #include "graphics/Screen.h" #include "graphics/emotes.h" #include diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index d3fcbbf9d..7d638dd71 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -40,10 +40,7 @@ bool RotaryEncoderImpl::init() int32_t RotaryEncoderImpl::runOnce() { - InputEvent e; - e.inputEvent = INPUT_BROKER_NONE; - e.source = this->originName; - + 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()) { @@ -70,7 +67,7 @@ int32_t RotaryEncoderImpl::runOnce() this->notifyObservers(&e); } - return 20; + return 10; } #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6667f9f17..8d576f008 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1506,6 +1506,9 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() deviceMetadata.hw_model = HW_VENDOR; deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; +#if MESHTASTIC_EXCLUDE_MQTT + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_MQTT_CONFIG; +#endif #if MESHTASTIC_EXCLUDE_REMOTEHARDWARE deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; #endif @@ -1528,10 +1531,21 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; #endif -#ifndef ARCH_ESP32 +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER + // PAXCOUNTER is only supported on ESP32 due to memory constraints +#else deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; #endif -#if !defined(HAS_RGB_LED) && !RAK_4631 +#if MESHTASTIC_EXCLUDE_STOREFORWARD + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_STOREFORWARD_CONFIG; +#endif +#if MESHTASTIC_EXCLUDE_RANGETEST + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_RANGETEST_CONFIG; +#endif +#if MESHTASTIC_EXCLUDE_NEIGHBORINFO + deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NEIGHBORINFO_CONFIG; +#endif +#if (!defined(HAS_RGB_LED) && !defined(RAK_4631)) || defined(MESHTASTIC_EXCLUDE_AMBIENTLIGHTING) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; #endif diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 794b25aa6..7ceca2195 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -74,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; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 2ab6fda59..52a18a53f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -775,7 +775,9 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.version = DEVICESTATE_CUR_VER; moduleConfig.has_mqtt = true; +#if !MESHTASTIC_EXCLUDE_RANGETEST moduleConfig.has_range_test = true; +#endif moduleConfig.has_serial = true; moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; @@ -841,6 +843,12 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; +#if !MESHTASTIC_EXCLUDE_AUDIO + moduleConfig.has_audio = true; +#endif +#if !MESHTASTIC_EXCLUDE_PAXCOUNTER + moduleConfig.has_paxcounter = true; +#endif #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif @@ -883,12 +891,14 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; moduleConfig.detection_sensor.minimum_broadcast_secs = 45; +#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING moduleConfig.has_ambient_lighting = true; moduleConfig.ambient_lighting.current = 10; // Default to a color based on our node number moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; +#endif initModuleConfigIntervals(); } @@ -1428,15 +1438,25 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) moduleConfig.has_canned_message = true; moduleConfig.has_external_notification = true; moduleConfig.has_mqtt = true; +#if !MESHTASTIC_EXCLUDE_RANGETEST moduleConfig.has_range_test = true; +#endif moduleConfig.has_serial = true; +#if !MESHTASTIC_EXCLUDE_STOREFORWARD moduleConfig.has_store_forward = true; +#endif moduleConfig.has_telemetry = true; moduleConfig.has_neighbor_info = true; moduleConfig.has_detection_sensor = true; +#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING moduleConfig.has_ambient_lighting = true; +#endif +#if !MESHTASTIC_EXCLUDE_AUDIO moduleConfig.has_audio = true; +#endif +#if !MESHTASTIC_EXCLUDE_PAXCOUNTER moduleConfig.has_paxcounter = true; +#endif success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); 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..d11eff9e7 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -34,6 +34,21 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; +// Helper function to skip excluded module configs and advance state +size_t PhoneAPI::skipExcludedModuleConfig(uint8_t *buf) +{ + config_state++; + if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { + if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { + state = STATE_SEND_FILEMANIFEST; + } else { + state = STATE_SEND_OTHER_NODEINFOS; + } + config_state = 0; + } + return getFromRadio(buf); +} + PhoneAPI::PhoneAPI() { lastContactMsec = millis(); @@ -354,20 +369,35 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; break; case meshtastic_ModuleConfig_external_notification_tag: +#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION) LOG_DEBUG("Send module config: ext notification"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; break; +#else + LOG_DEBUG("External Notification module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif case meshtastic_ModuleConfig_store_forward_tag: +#if !MESHTASTIC_EXCLUDE_STOREFORWARD LOG_DEBUG("Send module config: store forward"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; break; +#else + LOG_DEBUG("Store & Forward module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif case meshtastic_ModuleConfig_range_test_tag: +#if !MESHTASTIC_EXCLUDE_RANGETEST LOG_DEBUG("Send module config: range test"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; break; +#else + LOG_DEBUG("Range Test module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif case meshtastic_ModuleConfig_telemetry_tag: LOG_DEBUG("Send module config: telemetry"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; @@ -379,10 +409,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_ModuleConfig_audio_tag: +#if !MESHTASTIC_EXCLUDE_AUDIO LOG_DEBUG("Send module config: audio"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; break; +#else + LOG_DEBUG("Audio module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif case meshtastic_ModuleConfig_remote_hardware_tag: LOG_DEBUG("Send module config: remote hardware"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; @@ -399,15 +434,25 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; break; case meshtastic_ModuleConfig_ambient_lighting_tag: +#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING LOG_DEBUG("Send module config: ambient lighting"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; break; +#else + LOG_DEBUG("Ambient Lighting module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif case meshtastic_ModuleConfig_paxcounter_tag: +#if !MESHTASTIC_EXCLUDE_PAXCOUNTER LOG_DEBUG("Send module config: paxcounter"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; break; +#else + LOG_DEBUG("Paxcounter module excluded from build, skipping"); + return skipExcludedModuleConfig(buf); +#endif default: LOG_ERROR("Unknown module config type %d", config_state); } diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 0d7772d17..6b4bb6fc1 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -172,4 +172,7 @@ class PhoneAPI /// If the mesh service tells us fromNum has changed, tell the phone virtual int onNotify(uint32_t newValue) override; + + /// Helper function to skip excluded module configs and advance state + size_t skipExcludedModuleConfig(uint8_t *buf); }; 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/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index ce3722aa7..2a4e77870 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -272,6 +272,8 @@ 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, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 42ebb8417..74953d8fc 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -342,6 +342,11 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(value->Stringify().c_str()); delete value; + + // Clean up the fileList to prevent memory leak + for (auto *val : fileList) { + delete val; + } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -610,33 +615,38 @@ 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);
+        // Clean up original array to prevent memory leak
+        for (auto *val : tempArray) {
+            delete val;
+        }
+        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()));
@@ -765,6 +775,11 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().c_str());
     delete value;
+
+    // Clean up the nodesArray to prevent memory leak
+    for (auto *val : nodesArray) {
+        delete val;
+    }
 }
 
 /*
@@ -955,5 +970,10 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
     JSONValue *value = new JSONValue(jsonObjOuter);
     res->print(value->Stringify().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/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index 407003f7e..78c101765 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -1040,19 +1040,32 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
             res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
             break;
         case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG:
+#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
             LOG_INFO("Get module config: External Notification");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
             res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
+#else
+            LOG_DEBUG("External Notification module excluded from build, skipping config");
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG:
+#if !MESHTASTIC_EXCLUDE_STOREFORWARD
             LOG_INFO("Get module config: Store & Forward");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
             res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
+#else
+            LOG_DEBUG("Store & Forward module excluded from build, skipping config");
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG:
+#if !MESHTASTIC_EXCLUDE_RANGETEST
             LOG_INFO("Get module config: Range Test");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
             res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
+#else
+            LOG_DEBUG("Range Test module excluded from build, skipping config");
+            // Don't set payload variant - will result in empty response
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG:
             LOG_INFO("Get module config: Telemetry");
@@ -1065,9 +1078,13 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
             res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
             break;
         case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG:
+#if !MESHTASTIC_EXCLUDE_AUDIO
             LOG_INFO("Get module config: Audio");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
             res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
+#else
+            LOG_DEBUG("Audio module excluded from build, skipping config");
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG:
             LOG_INFO("Get module config: Remote Hardware");
@@ -1080,19 +1097,31 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
             res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
             break;
         case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG:
+#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR)
             LOG_INFO("Get module config: Detection Sensor");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
             res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
+#else
+            LOG_DEBUG("Detection Sensor module excluded from build, skipping config");
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG:
+#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING
             LOG_INFO("Get module config: Ambient Lighting");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
             res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
+#else
+            LOG_DEBUG("Ambient Lighting module excluded from build, skipping config");
+#endif
             break;
         case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG:
+#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
             LOG_INFO("Get module config: Paxcounter");
             res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
             res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
+#else
+            LOG_DEBUG("Paxcounter module excluded from build, skipping config");
+#endif
             break;
         }
 
diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp
index c90d9250f..8ac160f8b 100644
--- a/src/modules/Telemetry/EnvironmentTelemetry.cpp
+++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp
@@ -30,7 +30,8 @@
 
 namespace graphics
 {
-extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
+extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
+                             bool show_date);
 }
 #if __has_include()
 #include "Sensor/AHT10.h"
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index 35409edef..479861a2e 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -24,7 +24,8 @@
 
 namespace graphics
 {
-extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
+extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
+                             bool show_date);
 }
 
 int32_t PowerTelemetryModule::runOnce()
diff --git a/src/sleep.cpp b/src/sleep.cpp
index bff318900..83597e349 100644
--- a/src/sleep.cpp
+++ b/src/sleep.cpp
@@ -431,15 +431,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
 #endif
 
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power down XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, LOW);
-    io.digitalWrite(EXPANDS_AMP_EN, LOW);
-    io.digitalWrite(EXPANDS_KB_EN, LOW);
-    io.digitalWrite(EXPANDS_SD_EN, LOW);
-    io.digitalWrite(EXPANDS_GPIO_EN, LOW);
-#endif
-
     auto res = esp_sleep_enable_gpio_wakeup();
     if (res != ESP_OK) {
         LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
@@ -480,14 +471,6 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
         gpio_wakeup_disable((gpio_num_t)RF95_IRQ);
     }
 #endif
-#ifdef T_LORA_PAGER
-    LOG_DEBUG("power up XL9555 io");
-    io.digitalWrite(EXPANDS_DRV_EN, HIGH);
-    io.digitalWrite(EXPANDS_AMP_EN, HIGH);
-    io.digitalWrite(EXPANDS_KB_EN, HIGH);
-    io.digitalWrite(EXPANDS_SD_EN, HIGH);
-    io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
-#endif
 
     esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
     notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
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/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini
index d5847959b..4131cc30a 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
@@ -26,6 +26,7 @@ extends = env:picomputer-s3
 
 build_flags =
   ${env:picomputer-s3.build_flags}
+  -D MESHTASTIC_EXCLUDE_WEBSERVER=1
   -D INPUTDRIVER_MATRIX_TYPE=1
   -D USE_PIN_BUZZER=PIN_BUZZER
   -D USE_SX127x
diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
index 8b86e0217..de4714efa 100644
--- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
+++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini
@@ -70,6 +70,7 @@ build_flags =
   ${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
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/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini
index b16e516a7..312d46259 100644
--- a/variants/esp32s3/tlora-pager/platformio.ini
+++ b/variants/esp32s3/tlora-pager/platformio.ini
@@ -26,7 +26,7 @@ lib_deps = ${esp32s3_base.lib_deps}
   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
+  https://github.com/mverch67/RotaryEncoder/archive/25a59d5745a6645536f921427d80b08e78f886d4.zip
 
 [env:tlora-pager-tft]
 board_level = extra
diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini
index 476858ff5..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}
@@ -41,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
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/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 b71106a53..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.99F)
+#define ADC_MULTIPLIER (4.916F)
 
 #define HAS_RTC 0
 #ifdef __cplusplus
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/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}