diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index bc4d248db..71e88dbff 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -76,7 +76,7 @@ bool loopCanSleep() // Called just prior to starting Meshtastic. Allows for setting config values before startup. void lateInitVariant() { - settingsMap[logoutputlevel] = level_error; + portduino_config.logoutputlevel = level_error; channelFile.channels[0] = meshtastic_Channel{ .has_settings = true, .settings = @@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht // Start Meshtastic in a thread and wait till it has reached the ON state. int LLVMFuzzerInitialize(int *argc, char ***argv) { - settingsMap[maxtophone] = 5; + portduino_config.maxtophone = 5; meshtasticThread = std::thread([program = *argv[0]]() { char nodeIdStr[12]; 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 651e25b2a..e10e20a04 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,13 +8,13 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.467 - - renovate@41.90.1 + - checkov@3.2.469 + - 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/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/bin/device-install.bat b/bin/device-install.bat index 93b2fcec1..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 @@ -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" 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 bebbc285e..108ca4910 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 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/debian/changelog b/debian/changelog index 3bb0de79c..29841d0db 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.8.0) UNRELEASED; urgency=medium +meshtasticd (2.7.9.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -44,4 +44,7 @@ meshtasticd (2.7.8.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Sat, 30 Aug 2025 00:26:04 +0000 + [ ] + * 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 ef0fef791..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/a3e0e1be372d069f47b4c19d718f5267251744d7.zip + https://github.com/meshtastic/device-ui/archive/233d18ef42e9d189f90fdfe621f0cd7edff2d221.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 4c4427c4a..a84657c22 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 +Subproject commit a84657c220421536f18d11fc5edf680efadbceeb diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index 1c081ae29..d65c4f1e8 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess { int result; #ifdef ARCH_PORTDUINO - bool utf = !settingsMap[ascii_logs]; + bool utf = !portduino_config.ascii_logs; #else bool utf = true; #endif diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d367aa661..b2749806c 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -38,4 +38,46 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC return useShortName ? "Custom" : "Invalid"; break; } +} + +const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + switch (role) { + case meshtastic_Config_DeviceConfig_Role_CLIENT: + return "Client"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: + return "Client Mute"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: + return "Client Hidden"; + break; + case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: + return "Lost and Found"; + break; + case meshtastic_Config_DeviceConfig_Role_TRACKER: + return "Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_SENSOR: + return "Sensor"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK: + return "TAK"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: + return "TAK Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER: + return "Router"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: + return "Router Late"; + break; + case meshtastic_Config_DeviceConfig_Role_REPEATER: + return "Repeater"; + break; + default: + return "Unknown"; + break; + } } \ No newline at end of file diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index 2d7a3e8db..ad193e966 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -6,4 +6,7 @@ class DisplayFormatters public: static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); + + public: + static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; diff --git a/src/GPSStatus.h b/src/GPSStatus.h index 4b7997935..a1a9f2c56 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -22,6 +22,9 @@ class GPSStatus : public Status meshtastic_Position p = meshtastic_Position_init_default; + /// Time of last valid GPS fix (millis since boot) + uint32_t lastFixMillis = 0; + public: GPSStatus() { statusType = STATUS_TYPE_GPS; } @@ -83,6 +86,9 @@ class GPSStatus : public Status uint32_t getNumSatellites() const { return p.sats_in_view; } + /// Return millis() when the last GPS fix occurred (0 = never) + uint32_t getLastFixMillis() const { return lastFixMillis; } + bool matches(const GPSStatus *newStatus) const { #ifdef GPS_DEBUG @@ -114,6 +120,9 @@ class GPSStatus : public Status if (isDirty) { if (hasLock) { + // Record time of last valid GPS fix + lastFixMillis = millis(); + // In debug logs, identify position by @timestamp:stage (stage 3 = notify) LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, 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/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 7c8d77651..efab84399 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -57,7 +57,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l #endif #ifdef ARCH_PORTDUINO - bool color = !settingsMap[ascii_logs]; + bool color = !portduino_config.ascii_logs; #else bool color = true; #endif @@ -99,7 +99,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, size_t r = 0; #ifdef ARCH_PORTDUINO - bool color = !settingsMap[ascii_logs]; + bool color = !portduino_config.ascii_logs; #else bool color = true; #endif @@ -288,7 +288,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #if ARCH_PORTDUINO // level trace is special, two possible ways to handle it. if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - if (settingsStrings[traceFilename] != "") { + if (portduino_config.traceFilename != "") { va_list arg; va_start(arg, format); try { @@ -297,18 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) } va_end(arg); } - if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { delete[] newFormat; return; } } - if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { delete[] newFormat; return; - } else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { + } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { delete[] newFormat; return; - } else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { + } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { delete[] newFormat; return; } 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/detect/ScanI2C.h b/src/detect/ScanI2C.h index e46c6f623..470a416c0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -80,6 +80,7 @@ class ScanI2C LTR553ALS, BHI260AP, BMM150, + TSL2561, DRV2605 } DeviceType; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 9aef9defe..5cb4fca32 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -461,7 +461,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); diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 9ae7ae97d..7a253ff50 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -808,6 +808,14 @@ bool GPS::setup() } else { LOG_INFO("GNSS module configuration saved!"); } + } else if (gnssModel == GNSS_MODEL_CM121) { + // only ask for RMC and GGA + // enable GGA + _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); + delay(250); + // enable RMC + _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); + delay(250); } didSerialInit = true; } @@ -1240,9 +1248,15 @@ GnssModel_t GPS::probe(int serialSpeed) _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); delay(20); + // Close NMEA sequences on CM121 + _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); + _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); + _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); + delay(20); - // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A - std::vector unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}}; + // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 + std::vector unicore = { + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); std::vector atgm = { @@ -1422,7 +1436,7 @@ GPS *GPS::createGps() _en_gpio = PIN_GPS_EN; #endif #ifdef ARCH_PORTDUINO - if (!settingsMap[has_gps]) + if (!portduino_config.has_gps) return nullptr; #endif if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all @@ -1532,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/GPS.h b/src/gps/GPS.h index 177cfe74b..1233003c8 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -31,7 +31,8 @@ typedef enum { GNSS_MODEL_MTK_PA1616S, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352, - GNSS_MODEL_LS20031 + GNSS_MODEL_LS20031, + GNSS_MODEL_CM121 } GnssModel_t; typedef enum { 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/Screen.cpp b/src/graphics/Screen.cpp index 76c423133..eb8093947 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -370,7 +370,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (settingsMap[displayPanel] != no_screen) { + if (portduino_config.displayPanel != no_screen) { LOG_DEBUG("Make TFTDisplay!"); dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -618,7 +618,7 @@ void Screen::setup() #if ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (settingsMap[touchscreenModule]) { + if (portduino_config.touchscreenModule) { touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); @@ -1018,6 +1018,11 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(digital_icon_clock); } #endif + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(small_chirpy); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!hiddenFrames.wifi && isWifiAvailable()) { @@ -1186,6 +1191,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName) if (frameName == "show_favorites") { hiddenFrames.show_favorites = !hiddenFrames.show_favorites; } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } } bool Screen::isFrameHidden(const std::string &frameName) const @@ -1214,6 +1222,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.clock; if (frameName == "show_favorites") return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; return false; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 8c13bcf9a..55ce20052 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -669,6 +669,7 @@ class Screen : public concurrency::OSThread uint8_t nodelist_distance = 255; uint8_t nodelist_bearings = 255; uint8_t clock = 255; + uint8_t chirpy = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; uint8_t lora = 255; @@ -698,6 +699,7 @@ class Screen : public concurrency::OSThread #endif bool lora = false; bool show_favorites = false; + bool chirpy = true; } hiddenFrames; /// Try to start drawing ASAP diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index b458e54e4..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" @@ -16,6 +17,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth) isHighResolution = true; } + if (screenwidth > 128 && screenheight <= 64) { + isHighResolution = false; + } + // Special case for Heltec Wireless Tracker v1.1 if (screenwidth == 160 && screenheight == 80) { isHighResolution = false; @@ -53,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; @@ -69,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); @@ -187,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; @@ -202,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 === @@ -229,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); @@ -244,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); @@ -287,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 b1814005e..3eeb17ef0 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -767,24 +767,24 @@ class LGFX : public lgfx::LGFX_Device LGFX(void) { - if (settingsMap[displayPanel] == st7789) + if (portduino_config.displayPanel == st7789) _panel_instance = new lgfx::Panel_ST7789; - else if (settingsMap[displayPanel] == st7735) + else if (portduino_config.displayPanel == st7735) _panel_instance = new lgfx::Panel_ST7735; - else if (settingsMap[displayPanel] == st7735s) + else if (portduino_config.displayPanel == st7735s) _panel_instance = new lgfx::Panel_ST7735S; - else if (settingsMap[displayPanel] == st7796) + else if (portduino_config.displayPanel == st7796) _panel_instance = new lgfx::Panel_ST7796; - else if (settingsMap[displayPanel] == ili9341) + else if (portduino_config.displayPanel == ili9341) _panel_instance = new lgfx::Panel_ILI9341; - else if (settingsMap[displayPanel] == ili9342) + else if (portduino_config.displayPanel == ili9342) _panel_instance = new lgfx::Panel_ILI9342; - else if (settingsMap[displayPanel] == ili9488) + else if (portduino_config.displayPanel == ili9488) _panel_instance = new lgfx::Panel_ILI9488; - else if (settingsMap[displayPanel] == hx8357d) + else if (portduino_config.displayPanel == hx8357d) _panel_instance = new lgfx::Panel_HX8357D; #if defined(LGFX_SDL) - else if (settingsMap[displayPanel] == x11) { + else if (portduino_config.displayPanel == x11) { _panel_instance = new lgfx::Panel_sdl; } #endif @@ -795,61 +795,61 @@ class LGFX : public lgfx::LGFX_Device auto buscfg = _bus_instance.config(); buscfg.spi_mode = 0; - buscfg.spi_host = settingsMap[displayspidev]; + buscfg.spi_host = portduino_config.display_spi_dev_int; - buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable) + buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) _bus_instance.config(buscfg); // applies the set value to the bus. _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]); - cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = settingsMap[displayReset]; - if (settingsMap[displayRotate]) { - cfg.panel_width = settingsMap[displayHeight]; // actual displayable width - cfg.panel_height = settingsMap[displayWidth]; // actual displayable height + LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); + cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = portduino_config.displayReset.pin; + if (portduino_config.displayRotate) { + cfg.panel_width = portduino_config.displayHeight; // actual displayable width + cfg.panel_height = portduino_config.displayWidth; // actual displayable height } else { - cfg.panel_width = settingsMap[displayWidth]; // actual displayable width - cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + cfg.panel_width = portduino_config.displayWidth; // actual displayable width + cfg.panel_height = portduino_config.displayHeight; // actual displayable height } - cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction - cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction - cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) - cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed + cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction + cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction + cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed _panel_instance->config(cfg); // Configure settings for touch control. - if (settingsMap[touchscreenModule]) { - if (settingsMap[touchscreenModule] == xpt2046) { + if (portduino_config.touchscreenModule) { + if (portduino_config.touchscreenModule == xpt2046) { _touch_instance = new lgfx::Touch_XPT2046; - } else if (settingsMap[touchscreenModule] == stmpe610) { + } else if (portduino_config.touchscreenModule == stmpe610) { _touch_instance = new lgfx::Touch_STMPE610; - } else if (settingsMap[touchscreenModule] == ft5x06) { + } else if (portduino_config.touchscreenModule == ft5x06) { _touch_instance = new lgfx::Touch_FT5x06; } auto touch_cfg = _touch_instance->config(); - touch_cfg.pin_cs = settingsMap[touchscreenCS]; + touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; touch_cfg.x_min = 0; - touch_cfg.x_max = settingsMap[displayHeight] - 1; + touch_cfg.x_max = portduino_config.displayHeight - 1; touch_cfg.y_min = 0; - touch_cfg.y_max = settingsMap[displayWidth] - 1; - touch_cfg.pin_int = settingsMap[touchscreenIRQ]; + touch_cfg.y_max = portduino_config.displayWidth - 1; + touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = settingsMap[touchscreenRotate]; - if (settingsMap[touchscreenI2CAddr] != -1) { - touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr]; + touch_cfg.offset_rotation = portduino_config.touchscreenRotate; + if (portduino_config.touchscreenI2CAddr != -1) { + touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; } else { - touch_cfg.spi_host = settingsMap[touchscreenspidev]; + touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; } _touch_instance->config(touch_cfg); _panel_instance->setTouch(_touch_instance); } #if defined(LGFX_SDL) - if (settingsMap[displayPanel] == x11) { + if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; sdl_panel_->setup(); sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); @@ -1115,10 +1115,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g backlightEnable = p; #if ARCH_PORTDUINO - if (settingsMap[displayRotate]) { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + if (portduino_config.displayRotate) { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); } else { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); } #elif defined(SCREEN_ROTATE) @@ -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) { @@ -1231,7 +1240,7 @@ void TFTDisplay::sdlLoop() #if defined(LGFX_SDL) static int lastPressed = 0; static int shuttingDown = false; - if (settingsMap[displayPanel] == x11) { + if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; if (sdl_panel_->loop() && !shuttingDown) { LOG_WARN("Window Closed!"); @@ -1279,8 +1288,8 @@ void TFTDisplay::sendCommand(uint8_t com) backlightEnable->set(true); #if ARCH_PORTDUINO display(true); - if (settingsMap[displayBacklight] > 0) - digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->wakeup(); tft->powerSaveOff(); @@ -1303,8 +1312,8 @@ void TFTDisplay::sendCommand(uint8_t com) backlightEnable->set(false); #if ARCH_PORTDUINO tft->clear(); - if (settingsMap[displayBacklight] > 0) - digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->sleep(); tft->powerSaveOn(); 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 08466662c..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,7 +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); + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -294,6 +293,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, isPM ? "pm" : "am"); } + #ifndef USE_EINK xOffset = (isHighResolution) ? 18 : 10; display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, @@ -313,7 +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); + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 3e4030e0f..c93ef578c 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -391,18 +391,27 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Second Row: Radio Preset === + // === Second Row: Role === + auto role = DisplayFormatters::getDeviceRole(config.device.role); + char device_role[25]; + snprintf(device_role, sizeof(device_role), "Role: %s", role); + textWidth = display->getStringWidth(device_role); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], device_role); + + // === Third Row: Radio Preset === 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) { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + snprintf(regionradiopreset, sizeof(regionradiopreset), "Reg: %s/%s", region, mode); } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); - // === Third Row: Frequency / ChanNum === + // === Fourth Row: Frequency / ChanNum === char frequencyslot[35]; char freqStr[16]; float freq = RadioLibInterface::instance->getFreq(); @@ -420,7 +429,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); - // === Fourth Row: Channel Utilization === + // === Fifth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); @@ -437,7 +446,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, getTextPositions(display)[line++], chUtil); + display->drawString(starting_position, getTextPositions(display)[line], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { @@ -474,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); } @@ -625,6 +634,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x display->drawString(nameX, getTextPositions(display)[line], uptimeStr); } } + +// **************************** +// * Chirpy Screen * +// **************************** +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); + int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; + int textX_offset = 10; + if (isHighResolution) { + iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); + iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; + textX_offset = textX_offset * 4; + display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); + } else { + display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); + } + + int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); + display->drawString(textX, getTextPositions(display)[line++], "Hello"); + textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); + display->drawString(textX, getTextPositions(display)[line++], "World!"); +} } // namespace DebugRenderer } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 3382e931d..563a6c1ce 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -33,6 +33,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // System screen display void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Chirpy screen display +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e92a54751..dab3040f0 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -31,17 +31,19 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { - static const char *optionsArray[] = {"Back", "Region Picker"}; - enum optionsNumbers { Back = 0, lora_picker = 1 }; + static const char *optionsArray[] = {"Back", "Region Picker", "Device Role"}; + enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action } else if (selected == lora_picker) { menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == device_role_picker) { + menuHandler::menuQueue = menuHandler::device_role_picker; } }; screen->showOverlayBanner(bannerOptions); @@ -140,6 +142,40 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } +void menuHandler::DeviceRolePicker() +{ + static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; + enum optionsNumbers { + Back = 0, + devicerole_client = 1, + devicerole_clientmute = 2, + devicerole_lostandfound = 3, + devicerole_tracker = 4 + }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Device Role"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } else if (selected == devicerole_client) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else if (selected == devicerole_clientmute) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + } else if (selected == devicerole_lostandfound) { + config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + } else if (selected == devicerole_tracker) { + config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::TwelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; @@ -968,16 +1004,33 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - static const char *optionsArray[] = {"Back", "Number Picker"}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; + + optionsArray[options] = "Number Picker"; + optionsEnumArray[options++] = NumberPicker; + + optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; + optionsEnumArray[options++] = ShowChirpy; + BannerOverlayOptions bannerOptions; - std::string message = "Test to Run?\n"; - bannerOptions.message = message.c_str(); + bannerOptions.message = "Hidden Test Menu"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { + if (selected == NumberPicker) { menuQueue = number_test; screen->runNow(); + } else if (selected == ShowChirpy) { + screen->toggleFrameVisibility("chirpy"); + screen->setFrames(Screen::FOCUS_SYSTEM); + + } else { + menuQueue = system_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -1232,7 +1285,7 @@ void menuHandler::FrameToggles_menu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value - bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + bannerOptions.bannerCallback = [options](int selected) mutable -> void { // Find the index of selected in optionsEnumArray int idx = 0; for (; idx < options; ++idx) { @@ -1291,9 +1344,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) switch (menuQueue) { case menu_none: break; + case lora_Menu: + loraMenu(); + break; case lora_picker: LoraRegionPicker(); break; + case device_role_picker: + DeviceRolePicker(); + break; case no_timeout_lora_picker: LoraRegionPicker(0); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 00df22d6c..2be8e58a6 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -9,7 +9,9 @@ class menuHandler public: enum screenMenus { menu_none, + lora_Menu, lora_picker, + device_role_picker, no_timeout_lora_picker, TZ_picker, twelve_hour_picker, @@ -46,6 +48,7 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); + static void DeviceRolePicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 049722df8..e9da66712 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -879,7 +879,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) && + config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED); + + if (usePhoneGPS) { + // Phone-provided GPS is active + displayLine = "Phone GPS"; + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (isHighResolution) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // GPS disabled / not present if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { @@ -896,6 +915,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int xOffset = (isHighResolution) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { + // Onboard GPS UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); } @@ -922,32 +942,61 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // If GPS is off, no need to display these parts if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; - // === Second Row: Date === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char datetimeStr[25]; - bool showTime = false; // set to true for full datetime - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); - display->drawString(0, getTextPositions(display)[line++], fullLine); + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), "Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + } // === Third Row: Latitude === char latStr[32]; - snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); // === Fourth Row: Longitude === char lonStr[32]; - snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude === char DisplayLineTwo[32] = {0}; + int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) + ? ourNode->position.altitude + : geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); } 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/graphics/images.h b/src/graphics/images.h index e349cb6e0..168a9b716 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -288,5 +288,77 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; +#define chirpy_width 38 +#define chirpy_height 50 +static unsigned char chirpy[] = { + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, + 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, + 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, + 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, + 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, + 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, + 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, + 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, + 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + +#define chirpy_width_hirez 76 +#define chirpy_height_hirez 100 +static unsigned char chirpy_hirez[] = { + 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, + 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, + 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, + 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, + 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, + 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, + 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, + 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, + 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, + 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, + 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, + 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, + 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, + 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, + 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3}; + +#define chirpy_small_image_width 8 +#define chirpy_small_image_height 8 +static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; + #include "img/icon.xbm" static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index b2e92bdae..5654fa02a 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -41,78 +41,78 @@ void tftSetup(void) PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); #else - if (settingsMap[displayPanel] != no_screen) { + if (portduino_config.displayPanel != no_screen) { DisplayDriverConfig displayConfig; static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; #if defined(USE_X11) - if (settingsMap[displayPanel] == x11) { - if (settingsMap[displayWidth] && settingsMap[displayHeight]) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth], - (uint16_t)settingsMap[displayHeight]); + if (portduino_config.displayPanel == x11) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::X11); } else #elif defined(USE_FRAMEBUFFER) - if (settingsMap[displayPanel] == fb) { - if (settingsMap[displayWidth] && settingsMap[displayHeight]) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth], - (uint16_t)settingsMap[displayHeight]); + if (portduino_config.displayPanel == fb) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::FB); } else #endif { displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) - .panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]], - .panel_width = (uint16_t)settingsMap[displayWidth], - .panel_height = (uint16_t)settingsMap[displayHeight], - .rotation = (bool)settingsMap[displayRotate], - .pin_cs = (int16_t)settingsMap[displayCS], - .pin_rst = (int16_t)settingsMap[displayReset], - .offset_x = (uint16_t)settingsMap[displayOffsetX], - .offset_y = (uint16_t)settingsMap[displayOffsetY], - .offset_rotation = (uint8_t)settingsMap[displayOffsetRotate], - .invert = settingsMap[displayInvert] ? true : false, - .rgb_order = (bool)settingsMap[displayRGBOrder], - .dlen_16bit = settingsMap[displayPanel] == ili9486 || - settingsMap[displayPanel] == ili9488}) - .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency], + .panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], + .panel_width = (uint16_t)portduino_config.displayWidth, + .panel_height = (uint16_t)portduino_config.displayHeight, + .rotation = (bool)portduino_config.displayRotate, + .pin_cs = (int16_t)portduino_config.displayCS.pin, + .pin_rst = (int16_t)portduino_config.displayReset.pin, + .offset_x = (uint16_t)portduino_config.displayOffsetX, + .offset_y = (uint16_t)portduino_config.displayOffsetY, + .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, + .invert = portduino_config.displayInvert ? true : false, + .rgb_order = (bool)portduino_config.displayRGBOrder, + .dlen_16bit = portduino_config.displayPanel == ili9486 || + portduino_config.displayPanel == ili9488}) + .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency, .freq_read = 16000000, - .spi{.pin_dc = (int8_t)settingsMap[displayDC], + .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, .use_lock = true, - .spi_host = (uint16_t)settingsMap[displayspidev]}}) - .input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice], - .pointerDevice = settingsStrings[pointerDevice]}) - .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight], - .pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel], - .invert = (bool)settingsMap[displayBacklightInvert]}); - if (settingsMap[touchscreenI2CAddr] == -1) { + .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) + .input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, + .pointerDevice = portduino_config.pointerDevice}) + .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, + .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, + .invert = (bool)portduino_config.displayBacklightInvert}); + if (portduino_config.touchscreenI2CAddr == -1) { displayConfig.touch( - DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]], - .freq = (uint32_t)settingsMap[touchscreenBusFrequency], - .pin_int = (int16_t)settingsMap[touchscreenIRQ], - .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], + DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, .spi{ - .spi_host = (int8_t)settingsMap[touchscreenspidev], + .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, }, - .pin_cs = (int16_t)settingsMap[touchscreenCS]}); + .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); } else { displayConfig.touch(DisplayDriverConfig::touch_config_t{ - .type = touch[settingsMap[touchscreenModule]], - .freq = (uint32_t)settingsMap[touchscreenBusFrequency], + .type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, .x_min = 0, - .x_max = - (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) - - 1), + .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth + : portduino_config.displayHeight) - + 1), .y_min = 0, - .y_max = - (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) - - 1), - .pin_int = (int16_t)settingsMap[touchscreenIRQ], - .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], - .i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}}); + .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight + : portduino_config.displayWidth) - + 1), + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); } } deviceScreen = &DeviceScreen::create(&displayConfig); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 90f06ecc9..1f80fd5d3 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -33,9 +33,9 @@ int32_t LinuxInput::runOnce() { if (firstTime) { - if (settingsStrings[keyboardDevice] == "") + if (portduino_config.keyboardDevice == "") return disable(); - fd = open(settingsStrings[keyboardDevice].c_str(), O_RDWR); + fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); if (fd < 0) return disable(); ret = ioctl(fd, EVIOCGRAB, (void *)1); 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/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index cea47faeb..c0e220941 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -18,7 +18,7 @@ TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTo void TouchScreenImpl1::init() { #if ARCH_PORTDUINO - if (settingsMap[touchscreenModule]) { + if (portduino_config.touchscreenModule) { TouchScreenBase::init(true); inputBroker->registerSource(this); } else { diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 38be22f20..76a99f33d 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -6,7 +6,7 @@ #ifndef TB_DIRECTION #if ARCH_PORTDUINO #include "PortduinoGlue.h" -#define TB_DIRECTION (PinStatus) settingsMap[tbDirection] +#define TB_DIRECTION (PinStatus) portduino_config.lora_usb_vid #else #define TB_DIRECTION RISING #endif diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index 9b0926a1d..cb03eb4ff 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -13,7 +13,11 @@ void CardKbI2cImpl::init() 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, 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/main.cpp b/src/main.cpp index 73f68e95e..8d576f008 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -390,7 +390,7 @@ void setup() concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO - SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0); + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif @@ -535,9 +535,9 @@ void setup() #elif defined(I2C_SDA) && !defined(ARCH_RP2040) Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) - if (settingsStrings[i2cdev] != "") { - LOG_INFO("Use %s as I2C device", settingsStrings[i2cdev].c_str()); - Wire.begin(settingsStrings[i2cdev].c_str()); + if (portduino_config.i2cdev != "") { + LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); + Wire.begin(portduino_config.i2cdev.c_str()); } else { LOG_INFO("No I2C device configured, Skip"); } @@ -583,7 +583,7 @@ void setup() #if defined(I2C_SDA) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #elif defined(ARCH_PORTDUINO) - if (settingsStrings[i2cdev] != "") { + if (portduino_config.i2cdev != "") { LOG_INFO("Scan for i2c devices"); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); } @@ -743,6 +743,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 @@ -855,7 +856,7 @@ void setup() SPI.begin(false); #endif // HW_SPI1_DEVICE #elif ARCH_PORTDUINO - if (settingsStrings[spidev] != "ch341") { + if (portduino_config.lora_spi_dev != "ch341") { SPI.begin(); } #elif !defined(ARCH_ESP32) // ARCH_RP2040 @@ -881,7 +882,7 @@ void setup() defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen = new graphics::Screen(screen_found, screen_model, screen_geometry); } @@ -982,13 +983,13 @@ void setup() #endif #if defined(ARCH_PORTDUINO) - if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + if (portduino_config.userButtonPin.enabled) { - LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]); + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); UserButtonThread = new ButtonThread("UserButton"); if (screen) { ButtonConfig config; - config.pinNumber = (uint8_t)settingsMap[userButtonPin]; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; config.activeLow = true; config.activePullup = true; config.pullupSense = INPUT_PULLUP; @@ -1145,7 +1146,7 @@ void setup() if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } @@ -1161,15 +1162,10 @@ void setup() #endif #ifdef ARCH_PORTDUINO - const struct { - configNames cfgName; - std::string strName; - } loraModules[] = {{use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"}, - {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](configNames cfgName, LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, - RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) { - switch (cfgName) { + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { case use_rf95: return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); case use_sx1262: @@ -1186,31 +1182,34 @@ void setup() return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); case use_llcc68: return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; default: assert(0); // shouldn't happen return (RadioInterface *)nullptr; } }; - for (auto &loraModule : loraModules) { - if (settingsMap[loraModule.cfgName] && !rIf) { - LOG_DEBUG("Activate %s radio on SPI port %s", loraModule.strName.c_str(), settingsStrings[spidev].c_str()); - if (settingsStrings[spidev] == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); - } - rIf = loraModuleInterface(loraModule.cfgName, (LockingArduinoHal *)RadioLibHAL, settingsMap[cs_pin], - settingsMap[irq_pin], settingsMap[reset_pin], settingsMap[busy_pin]); - if (!rIf->init()) { - LOG_WARN("No %s radio", loraModule.strName.c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", loraModule.strName.c_str()); - } - } + + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); } + rIf = + loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); + + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } + #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); #else // HW_SPI1_DEVICE @@ -1232,20 +1231,6 @@ void setup() } #endif -#if defined(ARCH_PORTDUINO) - if (!rIf) { - rIf = new SimRadio; - if (!rIf->init()) { - LOG_WARN("No simulated radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("Use SIMULATED radio!"); - radioType = SIM_RADIO; - } - } -#endif - #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); @@ -1460,7 +1445,7 @@ void setup() #ifdef ARCH_PORTDUINO #if __has_include() - if (settingsMap[webserverport] != -1) { + if (portduino_config.webserverport != -1) { piwebServerThread = new PiWebServerThread(); std::atexit([] { delete piwebServerThread; }); } @@ -1521,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 @@ -1543,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/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a0d992c42..f83522c8b 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -21,7 +21,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO -#define LR1110_MAX_POWER settingsMap[lr1110_max_power] +#define LR1110_MAX_POWER portduino_config.lr1110_max_power #endif #ifndef LR1110_MAX_POWER #define LR1110_MAX_POWER 22 @@ -30,7 +30,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { // the 2.4G part maxes at 13dBm #if ARCH_PORTDUINO -#define LR1120_MAX_POWER settingsMap[lr1120_max_power] +#define LR1120_MAX_POWER portduino_config.lr1120_max_power #endif #ifndef LR1120_MAX_POWER #define LR1120_MAX_POWER 13 @@ -55,7 +55,7 @@ template bool LR11x0Interface::init() #endif #if ARCH_PORTDUINO - float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE #elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) float tcxoVoltage = diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 860250f75..7ceca2195 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; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index c8eba1b2e..52a18a53f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -673,7 +673,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #endif #elif ARCH_PORTDUINO bool hasScreen = false; - if (settingsMap[displayPanel]) + if (portduino_config.displayPanel) hasScreen = true; else hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; @@ -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(); } @@ -1334,8 +1344,8 @@ void NodeDB::loadFromDisk() } #if ARCH_PORTDUINO // set any config overrides - if (settingsMap[has_configDisplayMode]) { - config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)settingsMap[configDisplayMode]; + if (portduino_config.has_configDisplayMode) { + config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } #endif @@ -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/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 97f21fc34..0f32f3427 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -10,7 +10,7 @@ #endif #if ARCH_PORTDUINO -#define RF95_MAX_POWER settingsMap[rf95_max_power] +#define RF95_MAX_POWER portduino_config.rf95_max_power #endif #ifndef RF95_MAX_POWER #define RF95_MAX_POWER 20 @@ -94,16 +94,16 @@ void RF95Interface::setTransmitEnable(bool txon) #ifdef RF95_TXEN digitalWrite(RF95_TXEN, txon ? 1 : 0); #elif ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], txon ? 1 : 0); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); } #endif #ifdef RF95_RXEN digitalWrite(RF95_RXEN, txon ? 0 : 1); #elif ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], txon ? 0 : 1); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); } #endif } @@ -164,13 +164,13 @@ bool RF95Interface::init() digitalWrite(RF95_RXEN, 1); #endif #if ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[txen_pin], OUTPUT); - digitalWrite(settingsMap[txen_pin], 0); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, 0); } - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[rxen_pin], OUTPUT); - digitalWrite(settingsMap[rxen_pin], 0); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, 0); } #endif setTransmitEnable(false); diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 946b1982c..c18612101 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -417,7 +417,7 @@ void RadioLibInterface::handleReceiveInterrupt() int state = iface->readData((uint8_t *)&radioBuffer, length); #if ARCH_PORTDUINO - if (settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.logoutputlevel == level_trace) { printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); } #endif diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 6e5c6231b..e9ceeaef1 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -58,7 +58,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 c7e32c4a1..221e0275b 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -446,7 +446,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) #if ENABLE_JSON_LOGGING LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); #elif ARCH_PORTDUINO - if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); } #endif @@ -685,7 +685,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); #elif ARCH_PORTDUINO // Even ignored packets get logged in the trace - if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); } 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/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 729c1abc6..49dc562d4 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -12,7 +12,7 @@ // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and SX126x power config forgotten) #if ARCH_PORTDUINO -#define SX126X_MAX_POWER settingsMap[sx126x_max_power] +#define SX126X_MAX_POWER portduino_config.sx126x_max_power #endif #ifndef SX126X_MAX_POWER #define SX126X_MAX_POWER 22 @@ -53,10 +53,10 @@ template bool SX126xInterface::init() #endif #if ARCH_PORTDUINO - tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; - if (settingsMap[sx126x_ant_sw_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[sx126x_ant_sw_pin], HIGH); - pinMode(settingsMap[sx126x_ant_sw_pin], OUTPUT); + tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); + pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); } #endif if (tcxoVoltage == 0.0) @@ -98,7 +98,7 @@ template bool SX126xInterface::init() bool dio2AsRfSwitch = true; #elif defined(ARCH_PORTDUINO) bool dio2AsRfSwitch = false; - if (settingsMap[dio2_as_rf_switch]) { + if (portduino_config.dio2_as_rf_switch) { dio2AsRfSwitch = true; } #else @@ -112,9 +112,9 @@ template bool SX126xInterface::init() // no effect #if ARCH_PORTDUINO if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", settingsMap[rxen_pin], - settingsMap[txen_pin]); - lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]); + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, + portduino_config.lora_txen_pin.pin); + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #else #ifndef SX126X_RXEN diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 866426872..cbc98eeb1 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -11,7 +11,7 @@ // Particular boards might define a different max power based on what their hardware can do #if ARCH_PORTDUINO -#define SX128X_MAX_POWER settingsMap[sx128x_max_power] +#define SX128X_MAX_POWER portduino_config.sx128x_max_power #endif #ifndef SX128X_MAX_POWER #define SX128X_MAX_POWER 13 @@ -41,13 +41,13 @@ template bool SX128xInterface::init() #endif #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[rxen_pin], OUTPUT); - digitalWrite(settingsMap[rxen_pin], LOW); // Set low before becoming an output + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[txen_pin], OUTPUT); - digitalWrite(settingsMap[txen_pin], LOW); // Set low before becoming an output + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode @@ -93,8 +93,9 @@ template bool SX128xInterface::init() lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); } #elif ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE && settingsMap[rxen_pin] != RADIOLIB_NC && settingsMap[txen_pin] != RADIOLIB_NC) { - lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]); + if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && + portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #endif @@ -174,11 +175,11 @@ template void SX128xInterface::setStandby() LOG_ERROR("SX128x standby %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], LOW); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], LOW); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power @@ -210,11 +211,11 @@ template void SX128xInterface::addReceiveMetadata(meshtastic_Mes template void SX128xInterface::configHardwareForSend() { #if ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], HIGH); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); } - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], LOW); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } #else @@ -241,11 +242,11 @@ template void SX128xInterface::startReceive() setStandby(); #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], HIGH); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], LOW); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 8a68197f0..c8202bdc9 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 1d1ff47e0..2a4e77870 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -270,6 +270,10 @@ typedef enum _meshtastic_HardwareModel { /* MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices. https://heltec.org/project/meshsolar/ */ 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/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp
index 7d3542e83..3e9dbe8c2 100644
--- a/src/mesh/raspihttp/PiWebServer.cpp
+++ b/src/mesh/raspihttp/PiWebServer.cpp
@@ -65,8 +65,8 @@ mail:   marchammermann@googlemail.com
 #define DEFAULT_REALM "default_realm"
 #define PREFIX ""
 
-#define KEY_PATH settingsStrings[websslkeypath].c_str()
-#define CERT_PATH settingsStrings[websslcertpath].c_str()
+#define KEY_PATH portduino_config.webserver_ssl_key_path.c_str()
+#define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str()
 
 struct _file_config configWeb;
 
@@ -458,8 +458,8 @@ PiWebServerThread::PiWebServerThread()
         }
     }
 
-    if (settingsMap[webserverport] != 0) {
-        webservport = settingsMap[webserverport];
+    if (portduino_config.webserverport != 0) {
+        webservport = portduino_config.webserverport;
         LOG_INFO("Use webserver port from yaml config %i ", webservport);
     } else {
         LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443");
@@ -490,7 +490,7 @@ PiWebServerThread::PiWebServerThread()
         u_map_put(&configWeb.mime_types, ".ico", "image/x-icon");
         u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml");
 
-        webrootpath = settingsStrings[webserverrootpath];
+        webrootpath = portduino_config.webserver_root_path;
 
         configWeb.files_path = (char *)webrootpath.c_str();
         configWeb.url_prefix = "";
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 8926b171c..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"
@@ -198,6 +199,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 +304,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 +652,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/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp
index 8f10b9228..dcde495a2 100644
--- a/src/modules/Telemetry/HostMetrics.cpp
+++ b/src/modules/Telemetry/HostMetrics.cpp
@@ -9,11 +9,11 @@
 int32_t HostMetricsModule::runOnce()
 {
 #if ARCH_PORTDUINO
-    if (settingsMap[hostMetrics_interval] == 0) {
+    if (portduino_config.hostMetrics_interval == 0) {
         return disable();
     } else {
         sendMetrics();
-        return 60 * 1000 * settingsMap[hostMetrics_interval];
+        return 60 * 1000 * portduino_config.hostMetrics_interval;
     }
 #else
     return disable();
@@ -110,8 +110,8 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics()
             proc_loadavg.close();
         }
     }
-    if (settingsStrings[hostMetrics_user_command] != "") {
-        std::string userCommandResult = exec(settingsStrings[hostMetrics_user_command].c_str());
+    if (portduino_config.hostMetrics_user_command != "") {
+        std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str());
         if (userCommandResult.length() > 1) {
             strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string));
             t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0';
@@ -135,7 +135,7 @@ bool HostMetricsModule::sendMetrics()
     p->to = NODENUM_BROADCAST;
     p->decoded.want_response = false;
     p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
-    p->channel = settingsMap[hostMetrics_channel];
+    p->channel = portduino_config.hostMetrics_channel;
     LOG_INFO("Send packet to mesh");
     service->sendToMesh(p, RX_SRC_LOCAL, true);
     return true;
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/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/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp
index 929a45d09..b11d2547b 100644
--- a/src/platform/portduino/PortduinoGlue.cpp
+++ b/src/platform/portduino/PortduinoGlue.cpp
@@ -9,7 +9,6 @@
 #include "api/ServerAPI.h"
 #include "linux/gpio/LinuxGPIOPin.h"
 #include "meshUtils.h"
-#include "yaml-cpp/yaml.h"
 #include 
 #include 
 #include 
@@ -28,14 +27,13 @@
 
 #include "platform/portduino/USBHal.h"
 
-std::map settingsMap;
-std::map settingsStrings;
 portduino_config_struct portduino_config;
 std::ofstream traceFile;
 Ch341Hal *ch341Hal = nullptr;
 char *configPath = nullptr;
 char *optionMac = nullptr;
 bool verboseEnabled = false;
+bool yamlOnly = false;
 
 const char *argp_program_version = optstr(APP_VERSION);
 
@@ -75,6 +73,9 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state)
     case 'v':
         verboseEnabled = true;
         break;
+    case 'y':
+        yamlOnly = true;
+        break;
     case ARGP_KEY_ARG:
         return 0;
     default:
@@ -90,6 +91,7 @@ void portduinoCustomInit()
                                            {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"},
                                            {"sim", 's', 0, 0, "Run in Simulated radio mode"},
                                            {"verbose", 'v', 0, 0, "Set log level to full debug"},
+                                           {"output-yaml", 'y', 0, 0, "Output config yaml and exit"},
                                            {0}};
     static void *childArguments;
     static char doc[] = "Meshtastic native build.";
@@ -115,8 +117,8 @@ void getMacAddr(uint8_t *dmac)
             dmac[4] = hwId >> 8;
             dmac[5] = hwId & 0xff;
         }
-    } else if (settingsStrings[mac_address].length() > 11) {
-        MAC_from_string(settingsStrings[mac_address], dmac);
+    } else if (portduino_config.mac_address.length() > 11) {
+        MAC_from_string(portduino_config.mac_address, dmac);
         exit;
     } else {
 
@@ -148,89 +150,46 @@ void getMacAddr(uint8_t *dmac)
  */
 void portduinoSetup()
 {
-    printf("Set up Meshtastic on Portduino...\n");
     int max_GPIO = 0;
-    const configNames GPIO_lines[] = {cs_pin,
-                                      irq_pin,
-                                      busy_pin,
-                                      reset_pin,
-                                      sx126x_ant_sw_pin,
-                                      txen_pin,
-                                      rxen_pin,
-                                      displayDC,
-                                      displayCS,
-                                      displayBacklight,
-                                      displayBacklightPWMChannel,
-                                      displayReset,
-                                      touchscreenCS,
-                                      touchscreenIRQ,
-                                      userButtonPin,
-                                      tbUpPin,
-                                      tbDownPin,
-                                      tbLeftPin,
-                                      tbRightPin,
-                                      tbPressPin};
-
     std::string gpioChipName = "gpiochip";
-    settingsStrings[i2cdev] = "";
-    settingsStrings[keyboardDevice] = "";
-    settingsStrings[pointerDevice] = "";
-    settingsStrings[webserverrootpath] = "";
-    settingsStrings[spidev] = "";
-    settingsStrings[displayspidev] = "";
-    settingsMap[spiSpeed] = 2000000;
-    settingsMap[ascii_logs] = !isatty(1);
-    settingsMap[displayPanel] = no_screen;
-    settingsMap[touchscreenModule] = no_touchscreen;
-    settingsMap[tbUpPin] = RADIOLIB_NC;
-    settingsMap[tbDownPin] = RADIOLIB_NC;
-    settingsMap[tbLeftPin] = RADIOLIB_NC;
-    settingsMap[tbRightPin] = RADIOLIB_NC;
-    settingsMap[tbPressPin] = RADIOLIB_NC;
-
-    YAML::Node yamlConfig;
+    portduino_config.displayPanel = no_screen;
 
     if (portduino_config.force_simradio == true) {
-        settingsMap[use_simradio] = true;
+        portduino_config.lora_module = use_simradio;
     } else if (configPath != nullptr) {
         if (loadConfig(configPath)) {
-            std::cout << "Using " << configPath << " as config file" << std::endl;
+            if (!yamlOnly)
+                std::cout << "Using " << configPath << " as config file" << std::endl;
         } else {
             std::cout << "Unable to use " << configPath << " as config file" << std::endl;
             exit(EXIT_FAILURE);
         }
     } else if (access("config.yaml", R_OK) == 0) {
         if (loadConfig("config.yaml")) {
-            std::cout << "Using local config.yaml as config file" << std::endl;
+            if (!yamlOnly)
+                std::cout << "Using local config.yaml as config file" << std::endl;
         } else {
             std::cout << "Unable to use local config.yaml as config file" << std::endl;
             exit(EXIT_FAILURE);
         }
     } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) {
         if (loadConfig("/etc/meshtasticd/config.yaml")) {
-            std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
+            if (!yamlOnly)
+                std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl;
         } else {
             std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl;
             exit(EXIT_FAILURE);
         }
     } else {
-        std::cout << "No 'config.yaml' found..." << std::endl;
-        settingsMap[use_simradio] = true;
+        if (!yamlOnly)
+            std::cout << "No 'config.yaml' found..." << std::endl;
+        portduino_config.lora_module = use_simradio;
     }
 
-    if (settingsMap[use_simradio] == true) {
-        std::cout << "Running in simulated mode." << std::endl;
-        settingsMap[maxnodes] = 200;               // Default to 200 nodes
-        settingsMap[logoutputlevel] = level_debug; // Default to debug
-        // Set the random seed equal to TCPPort to have a different seed per instance
-        randomSeed(TCPPort);
-        return;
-    }
-
-    if (settingsStrings[config_directory] != "") {
+    if (portduino_config.config_directory != "") {
         std::string filetype = ".yaml";
         for (const std::filesystem::directory_entry &entry :
-             std::filesystem::directory_iterator{settingsStrings[config_directory]}) {
+             std::filesystem::directory_iterator{portduino_config.config_directory}) {
             if (ends_with(entry.path().string(), ".yaml")) {
                 std::cout << "Also using " << entry << " as additional config file" << std::endl;
                 loadConfig(entry.path().c_str());
@@ -238,15 +197,28 @@ void portduinoSetup()
         }
     }
 
+    if (yamlOnly) {
+        std::cout << portduino_config.emit_yaml() << std::endl;
+        exit(EXIT_SUCCESS);
+    }
+
+    if (portduino_config.lora_module == use_simradio) {
+        std::cout << "Running in simulated mode." << std::endl;
+        portduino_config.MaxNodes = 200; // Default to 200 nodes
+        // Set the random seed equal to TCPPort to have a different seed per instance
+        randomSeed(TCPPort);
+        return;
+    }
+
     // If LoRa `Module: auto` (default in config.yaml),
     // attempt to auto config based on Product Strings
-    if (settingsMap[use_autoconf] == true) {
+    if (portduino_config.lora_module == use_autoconf) {
         char autoconf_product[96] = {0};
         // Try CH341
         try {
             std::cout << "autoconf: Looking for CH341 device..." << std::endl;
-            ch341Hal =
-                new Ch341Hal(0, settingsStrings[lora_usb_serial_num], settingsMap[lora_usb_vid], settingsMap[lora_usb_pid]);
+            ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
+                                    portduino_config.lora_usb_pid);
             ch341Hal->getProductString(autoconf_product, 95);
             delete ch341Hal;
             std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl;
@@ -323,7 +295,7 @@ void portduinoSetup()
                         if (mac_start != nullptr) {
                             std::cout << "autoconf: Found mac data " << mac_start << std::endl;
                             if (strlen(mac_start) == 12)
-                                settingsStrings[mac_address] = std::string(mac_start);
+                                portduino_config.mac_address = std::string(mac_start);
                         }
                         if (devID_start != nullptr) {
                             std::cout << "autoconf: Found deviceid data " << devID_start << std::endl;
@@ -354,7 +326,7 @@ void portduinoSetup()
                 std::cerr << "autoconf: Unable to find config for " << autoconf_product << std::endl;
                 exit(EXIT_FAILURE);
             }
-            if (loadConfig((settingsStrings[available_directory] + product_config).c_str())) {
+            if (loadConfig((portduino_config.available_directory + product_config).c_str())) {
                 std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl;
             } else {
                 std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product
@@ -363,15 +335,16 @@ void portduinoSetup()
             }
         } else {
             std::cerr << "autoconf: Could not locate any devices" << std::endl;
+            exit(EXIT_FAILURE);
         }
     }
 
     // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address
     uint8_t dmac[6] = {0};
-    if (settingsStrings[spidev] == "ch341") {
+    if (portduino_config.lora_spi_dev == "ch341") {
         try {
-            ch341Hal =
-                new Ch341Hal(0, settingsStrings[lora_usb_serial_num], settingsMap[lora_usb_vid], settingsMap[lora_usb_pid]);
+            ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
+                                    portduino_config.lora_usb_pid);
         } catch (std::exception &e) {
             std::cerr << e.what() << std::endl;
             std::cerr << "Could not initialize CH341 device!" << std::endl;
@@ -383,7 +356,7 @@ void portduinoSetup()
         char product_string[96] = {0};
         ch341Hal->getProductString(product_string, 95);
         std::cout << "CH341 Product " << product_string << std::endl;
-        if (strlen(serial) == 8 && settingsStrings[mac_address].length() < 12) {
+        if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) {
             uint8_t hash[32] = {0};
             memcpy(hash, serial, 8);
             crypto->hash(hash, 8);
@@ -395,7 +368,7 @@ void portduinoSetup()
             dmac[5] = hash[5];
             char macBuf[13] = {0};
             sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
-            settingsStrings[mac_address] = macBuf;
+            portduino_config.mac_address = macBuf;
         }
     }
 
@@ -409,100 +382,38 @@ void portduinoSetup()
     // Rather important to set this, if not running simulated.
     randomSeed(time(NULL));
 
-    std::string defaultGpioChipName = gpioChipName + std::to_string(settingsMap[default_gpiochip]);
-
-    for (configNames i : GPIO_lines) {
-        if (settingsMap.count(i) && settingsMap[i] > max_GPIO)
-            max_GPIO = settingsMap[i];
+    std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip);
+    for (auto i : portduino_config.all_pins) {
+        if (i->enabled && i->pin > max_GPIO)
+            max_GPIO = i->pin;
     }
 
     gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need.
 
     // Need to bind all the configured GPIO pins so they're not simulated
     // TODO: If one of these fails, we should log and terminate
-    if (settingsMap.count(userButtonPin) > 0 && settingsMap[userButtonPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[userButtonPin], defaultGpioChipName, settingsMap[userButtonPin]) != ERRNO_OK) {
-            settingsMap[userButtonPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap.count(tbUpPin) > 0 && settingsMap[tbUpPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[tbUpPin], defaultGpioChipName, settingsMap[tbUpPin]) != ERRNO_OK) {
-            settingsMap[tbUpPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap.count(tbDownPin) > 0 && settingsMap[tbDownPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[tbDownPin], defaultGpioChipName, settingsMap[tbDownPin]) != ERRNO_OK) {
-            settingsMap[tbDownPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap.count(tbLeftPin) > 0 && settingsMap[tbLeftPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[tbLeftPin], defaultGpioChipName, settingsMap[tbLeftPin]) != ERRNO_OK) {
-            settingsMap[tbLeftPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap.count(tbRightPin) > 0 && settingsMap[tbRightPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[tbRightPin], defaultGpioChipName, settingsMap[tbRightPin]) != ERRNO_OK) {
-            settingsMap[tbRightPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap.count(tbPressPin) > 0 && settingsMap[tbPressPin] != RADIOLIB_NC) {
-        if (initGPIOPin(settingsMap[tbPressPin], defaultGpioChipName, settingsMap[tbPressPin]) != ERRNO_OK) {
-            settingsMap[tbPressPin] = RADIOLIB_NC;
-        }
-    }
-    if (settingsMap[displayPanel] != no_screen) {
-        if (settingsMap[displayCS] > 0)
-            initGPIOPin(settingsMap[displayCS], defaultGpioChipName, settingsMap[displayCS]);
-        if (settingsMap[displayDC] > 0)
-            initGPIOPin(settingsMap[displayDC], defaultGpioChipName, settingsMap[displayDC]);
-        if (settingsMap[displayBacklight] > 0)
-            initGPIOPin(settingsMap[displayBacklight], defaultGpioChipName, settingsMap[displayBacklight]);
-        if (settingsMap[displayReset] > 0)
-            initGPIOPin(settingsMap[displayReset], defaultGpioChipName, settingsMap[displayReset]);
-    }
-    if (settingsMap[touchscreenModule] != no_touchscreen) {
-        if (settingsMap[touchscreenCS] > 0)
-            initGPIOPin(settingsMap[touchscreenCS], defaultGpioChipName, settingsMap[touchscreenCS]);
-        if (settingsMap[touchscreenIRQ] > 0)
-            initGPIOPin(settingsMap[touchscreenIRQ], defaultGpioChipName, settingsMap[touchscreenIRQ]);
+    for (auto i : portduino_config.all_pins) {
+        if (i->enabled)
+            if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) {
+                printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line);
+                exit(EXIT_FAILURE);
+            }
     }
 
     // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware
-    if (settingsStrings[spidev] != "" && settingsStrings[spidev] != "ch341") {
-        const struct {
-            configNames pin;
-            configNames gpiochip;
-            configNames line;
-        } pinMappings[] = {{cs_pin, cs_gpiochip, cs_line},
-                           {irq_pin, irq_gpiochip, irq_line},
-                           {busy_pin, busy_gpiochip, busy_line},
-                           {reset_pin, reset_gpiochip, reset_line},
-                           {rxen_pin, rxen_gpiochip, rxen_line},
-                           {txen_pin, txen_gpiochip, txen_line},
-                           {sx126x_ant_sw_pin, sx126x_ant_sw_gpiochip, sx126x_ant_sw_line}};
-        for (auto &pinMap : pinMappings) {
-            auto setMapIter = settingsMap.find(pinMap.pin);
-            if (setMapIter != settingsMap.end() && setMapIter->second != RADIOLIB_NC) {
-                if (initGPIOPin(setMapIter->second, gpioChipName + std::to_string(settingsMap[pinMap.gpiochip]),
-                                settingsMap[pinMap.line]) != ERRNO_OK) {
-                    printf("Error setting pin number %d. It may not exist, or may already be in use.\n",
-                           settingsMap[pinMap.line]);
-                    exit(EXIT_FAILURE);
-                }
-            }
-        }
-        SPI.begin(settingsStrings[spidev].c_str());
+    if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") {
+        SPI.begin(portduino_config.lora_spi_dev.c_str());
     }
-    if (settingsStrings[traceFilename] != "") {
+    if (portduino_config.traceFilename != "") {
         try {
-            traceFile.open(settingsStrings[traceFilename], std::ios::out | std::ios::app);
+            traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app);
         } catch (std::ofstream::failure &e) {
             std::cout << "*** traceFile Exception " << e.what() << std::endl;
             exit(EXIT_FAILURE);
         }
     }
-    if (verboseEnabled && settingsMap[logoutputlevel] != level_trace) {
-        settingsMap[logoutputlevel] = level_debug;
+    if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
+        portduino_config.logoutputlevel = level_debug;
     }
 
     return;
@@ -537,99 +448,78 @@ bool loadConfig(const char *configPath)
         yamlConfig = YAML::LoadFile(configPath);
         if (yamlConfig["Logging"]) {
             if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") {
-                settingsMap[logoutputlevel] = level_trace;
+                portduino_config.logoutputlevel = level_trace;
             } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") {
-                settingsMap[logoutputlevel] = level_debug;
+                portduino_config.logoutputlevel = level_debug;
             } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") {
-                settingsMap[logoutputlevel] = level_info;
+                portduino_config.logoutputlevel = level_info;
             } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") {
-                settingsMap[logoutputlevel] = level_warn;
+                portduino_config.logoutputlevel = level_warn;
             } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") {
-                settingsMap[logoutputlevel] = level_error;
+                portduino_config.logoutputlevel = level_error;
             }
-            settingsStrings[traceFilename] = yamlConfig["Logging"]["TraceFile"].as("");
+            portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as("");
             if (yamlConfig["Logging"]["AsciiLogs"]) {
                 // Default is !isatty(1) but can be set explicitly in config.yaml
-                settingsMap[ascii_logs] = yamlConfig["Logging"]["AsciiLogs"].as();
+                portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as();
+                portduino_config.ascii_logs_explicit = true;
             }
         }
         if (yamlConfig["Lora"]) {
-            const struct {
-                configNames cfgName;
-                std::string strName;
-            } loraModules[] = {{use_simradio, "sim"},  {use_autoconf, "auto"}, {use_rf95, "RF95"},     {use_sx1262, "sx1262"},
-                               {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"},
-                               {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}};
-            for (auto &loraModule : loraModules) {
-                settingsMap[loraModule.cfgName] = false;
-            }
+
             if (yamlConfig["Lora"]["Module"]) {
-                for (auto &loraModule : loraModules) {
-                    if (yamlConfig["Lora"]["Module"].as("") == loraModule.strName) {
-                        settingsMap[loraModule.cfgName] = true;
+                for (auto &loraModule : portduino_config.loraModules) {
+                    if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) {
+                        portduino_config.lora_module = loraModule.first;
                         break;
                     }
                 }
             }
+            if (yamlConfig["Lora"]["SX126X_MAX_POWER"])
+                portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22);
+            if (yamlConfig["Lora"]["SX128X_MAX_POWER"])
+                portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13);
+            if (yamlConfig["Lora"]["LR1110_MAX_POWER"])
+                portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22);
+            if (yamlConfig["Lora"]["LR1120_MAX_POWER"])
+                portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13);
+            if (yamlConfig["Lora"]["RF95_MAX_POWER"])
+                portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20);
 
-            settingsMap[sx126x_max_power] = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22);
-            settingsMap[sx128x_max_power] = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13);
-            settingsMap[lr1110_max_power] = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22);
-            settingsMap[lr1120_max_power] = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13);
-            settingsMap[rf95_max_power] = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20);
+            if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio &&
+                !portduino_config.force_simradio) {
+                portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false);
+                portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000;
+                if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) {
+                    portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true"
+                }
 
-            settingsMap[dio2_as_rf_switch] = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false);
-            settingsMap[dio3_tcxo_voltage] = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000;
-            if (settingsMap[dio3_tcxo_voltage] == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) {
-                settingsMap[dio3_tcxo_voltage] = 1800; // default millivolts for "true"
-            }
-
-            // backwards API compatibility and to globally set gpiochip once
-            int defaultGpioChip = settingsMap[default_gpiochip] = yamlConfig["Lora"]["gpiochip"].as(0);
-
-            const struct {
-                configNames pin;
-                configNames gpiochip;
-                configNames line;
-                std::string strName;
-            } pinMappings[] = {
-                {cs_pin, cs_gpiochip, cs_line, "CS"},
-                {irq_pin, irq_gpiochip, irq_line, "IRQ"},
-                {busy_pin, busy_gpiochip, busy_line, "Busy"},
-                {reset_pin, reset_gpiochip, reset_line, "Reset"},
-                {txen_pin, txen_gpiochip, txen_line, "TXen"},
-                {rxen_pin, rxen_gpiochip, rxen_line, "RXen"},
-                {sx126x_ant_sw_pin, sx126x_ant_sw_gpiochip, sx126x_ant_sw_line, "SX126X_ANT_SW"},
-            };
-            for (auto &pinMap : pinMappings) {
-                if (yamlConfig["Lora"][pinMap.strName].IsMap()) {
-                    settingsMap[pinMap.pin] = yamlConfig["Lora"][pinMap.strName]["pin"].as(RADIOLIB_NC);
-                    settingsMap[pinMap.line] = yamlConfig["Lora"][pinMap.strName]["line"].as(settingsMap[pinMap.pin]);
-                    settingsMap[pinMap.gpiochip] = yamlConfig["Lora"][pinMap.strName]["gpiochip"].as(defaultGpioChip);
-                } else { // backwards API compatibility
-                    settingsMap[pinMap.pin] = yamlConfig["Lora"][pinMap.strName].as(RADIOLIB_NC);
-                    settingsMap[pinMap.line] = settingsMap[pinMap.pin];
-                    settingsMap[pinMap.gpiochip] = defaultGpioChip;
+                // backwards API compatibility and to globally set gpiochip once
+                portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0);
+                for (auto this_pin : portduino_config.all_pins) {
+                    if (this_pin->config_section == "Lora") {
+                        readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin);
+                    }
                 }
             }
 
-            settingsMap[spiSpeed] = yamlConfig["Lora"]["spiSpeed"].as(2000000);
-            settingsStrings[lora_usb_serial_num] = yamlConfig["Lora"]["USB_Serialnum"].as("");
-            settingsMap[lora_usb_pid] = yamlConfig["Lora"]["USB_PID"].as(0x5512);
-            settingsMap[lora_usb_vid] = yamlConfig["Lora"]["USB_VID"].as(0x1A86);
+            portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000);
+            portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as("");
+            portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512);
+            portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86);
 
-            settingsStrings[spidev] = yamlConfig["Lora"]["spidev"].as("spidev0.0");
-            if (settingsStrings[spidev] != "ch341") {
-                settingsStrings[spidev] = "/dev/" + settingsStrings[spidev];
-                if (settingsStrings[spidev].length() == 14) {
-                    int x = settingsStrings[spidev].at(11) - '0';
-                    int y = settingsStrings[spidev].at(13) - '0';
+            portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0");
+            if (portduino_config.lora_spi_dev != "ch341") {
+                portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev;
+                if (portduino_config.lora_spi_dev.length() == 14) {
+                    int x = portduino_config.lora_spi_dev.at(11) - '0';
+                    int y = portduino_config.lora_spi_dev.at(13) - '0';
                     // Pretty sure this is always true
                     if (x >= 0 && x < 10 && y >= 0 && y < 10) {
                         // I believe this bit of weirdness is specifically for the new GUI
-                        settingsMap[spidev] = x + y << 4;
-                        settingsMap[displayspidev] = settingsMap[spidev];
-                        settingsMap[touchscreenspidev] = settingsMap[spidev];
+                        portduino_config.lora_spi_dev_int = x + y << 4;
+                        portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int;
+                        portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int;
                     }
                 }
             }
@@ -676,163 +566,152 @@ bool loadConfig(const char *configPath)
                 }
             }
         }
-        if (yamlConfig["GPIO"]) {
-            settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC);
-        }
+        readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin);
         if (yamlConfig["GPS"]) {
             std::string serialPath = yamlConfig["GPS"]["SerialPath"].as("");
             if (serialPath != "") {
                 Serial1.setPath(serialPath);
-                settingsMap[has_gps] = 1;
+                portduino_config.has_gps = 1;
             }
         }
         if (yamlConfig["I2C"]) {
-            settingsStrings[i2cdev] = yamlConfig["I2C"]["I2CDevice"].as("");
+            portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as("");
         }
         if (yamlConfig["Display"]) {
-            if (yamlConfig["Display"]["Panel"].as("") == "ST7789")
-                settingsMap[displayPanel] = st7789;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ST7735")
-                settingsMap[displayPanel] = st7735;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ST7735S")
-                settingsMap[displayPanel] = st7735s;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ST7796")
-                settingsMap[displayPanel] = st7796;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ILI9341")
-                settingsMap[displayPanel] = ili9341;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ILI9342")
-                settingsMap[displayPanel] = ili9342;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ILI9486")
-                settingsMap[displayPanel] = ili9486;
-            else if (yamlConfig["Display"]["Panel"].as("") == "ILI9488")
-                settingsMap[displayPanel] = ili9488;
-            else if (yamlConfig["Display"]["Panel"].as("") == "HX8357D")
-                settingsMap[displayPanel] = hx8357d;
-            else if (yamlConfig["Display"]["Panel"].as("") == "X11")
-                settingsMap[displayPanel] = x11;
-            else if (yamlConfig["Display"]["Panel"].as("") == "FB")
-                settingsMap[displayPanel] = fb;
-            settingsMap[displayHeight] = yamlConfig["Display"]["Height"].as(0);
-            settingsMap[displayWidth] = yamlConfig["Display"]["Width"].as(0);
-            settingsMap[displayDC] = yamlConfig["Display"]["DC"].as(-1);
-            settingsMap[displayCS] = yamlConfig["Display"]["CS"].as(-1);
-            settingsMap[displayRGBOrder] = yamlConfig["Display"]["RGBOrder"].as(false);
-            settingsMap[displayBacklight] = yamlConfig["Display"]["Backlight"].as(-1);
-            settingsMap[displayBacklightInvert] = yamlConfig["Display"]["BacklightInvert"].as(false);
-            settingsMap[displayBacklightPWMChannel] = yamlConfig["Display"]["BacklightPWMChannel"].as(-1);
-            settingsMap[displayReset] = yamlConfig["Display"]["Reset"].as(-1);
-            settingsMap[displayOffsetX] = yamlConfig["Display"]["OffsetX"].as(0);
-            settingsMap[displayOffsetY] = yamlConfig["Display"]["OffsetY"].as(0);
-            settingsMap[displayRotate] = yamlConfig["Display"]["Rotate"].as(false);
-            settingsMap[displayOffsetRotate] = yamlConfig["Display"]["OffsetRotate"].as(1);
-            settingsMap[displayInvert] = yamlConfig["Display"]["Invert"].as(false);
-            settingsMap[displayBusFrequency] = yamlConfig["Display"]["BusFrequency"].as(40000000);
+
+            for (auto &screen_name : portduino_config.screen_names) {
+                if (yamlConfig["Display"]["Panel"].as("") == screen_name.second)
+                    portduino_config.displayPanel = screen_name.first;
+            }
+            portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0);
+            portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0);
+
+            readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1);
+            readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1);
+            readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1);
+            readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1);
+            readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1);
+
+            portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false);
+            portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false);
+            portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0);
+            portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0);
+            portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false);
+            portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1);
+            portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false);
+            portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000);
             if (yamlConfig["Display"]["spidev"]) {
-                settingsStrings[displayspidev] = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1");
-                if (settingsStrings[displayspidev].length() == 14) {
-                    int x = settingsStrings[displayspidev].at(11) - '0';
-                    int y = settingsStrings[displayspidev].at(13) - '0';
+                portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1");
+                if (portduino_config.display_spi_dev.length() == 14) {
+                    int x = portduino_config.display_spi_dev.at(11) - '0';
+                    int y = portduino_config.display_spi_dev.at(13) - '0';
                     if (x >= 0 && x < 10 && y >= 0 && y < 10) {
-                        settingsMap[displayspidev] = x + y << 4;
-                        settingsMap[touchscreenspidev] = settingsMap[displayspidev];
+                        portduino_config.display_spi_dev_int = x + y << 4;
+                        portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int;
                     }
                 }
             }
         }
         if (yamlConfig["Touchscreen"]) {
             if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046")
-                settingsMap[touchscreenModule] = xpt2046;
+                portduino_config.touchscreenModule = xpt2046;
             else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610")
-                settingsMap[touchscreenModule] = stmpe610;
+                portduino_config.touchscreenModule = stmpe610;
             else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911")
-                settingsMap[touchscreenModule] = gt911;
+                portduino_config.touchscreenModule = gt911;
             else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06")
-                settingsMap[touchscreenModule] = ft5x06;
-            settingsMap[touchscreenCS] = yamlConfig["Touchscreen"]["CS"].as(-1);
-            settingsMap[touchscreenIRQ] = yamlConfig["Touchscreen"]["IRQ"].as(-1);
-            settingsMap[touchscreenBusFrequency] = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000);
-            settingsMap[touchscreenRotate] = yamlConfig["Touchscreen"]["Rotate"].as(-1);
-            settingsMap[touchscreenI2CAddr] = yamlConfig["Touchscreen"]["I2CAddr"].as(-1);
+                portduino_config.touchscreenModule = ft5x06;
+
+            readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1);
+            readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1);
+
+            portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000);
+            portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1);
+            portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1);
             if (yamlConfig["Touchscreen"]["spidev"]) {
-                settingsStrings[touchscreenspidev] = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as("");
-                if (settingsStrings[touchscreenspidev].length() == 14) {
-                    int x = settingsStrings[touchscreenspidev].at(11) - '0';
-                    int y = settingsStrings[touchscreenspidev].at(13) - '0';
+                portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as("");
+                if (portduino_config.touchscreen_spi_dev.length() == 14) {
+                    int x = portduino_config.touchscreen_spi_dev.at(11) - '0';
+                    int y = portduino_config.touchscreen_spi_dev.at(13) - '0';
                     if (x >= 0 && x < 10 && y >= 0 && y < 10) {
-                        settingsMap[touchscreenspidev] = x + y << 4;
+                        portduino_config.touchscreen_spi_dev_int = x + y << 4;
                     }
                 }
             }
         }
         if (yamlConfig["Input"]) {
-            settingsStrings[keyboardDevice] = (yamlConfig["Input"]["KeyboardDevice"]).as("");
-            settingsStrings[pointerDevice] = (yamlConfig["Input"]["PointerDevice"]).as("");
-            settingsMap[userButtonPin] = yamlConfig["Input"]["User"].as(RADIOLIB_NC);
-            settingsMap[tbUpPin] = yamlConfig["Input"]["TrackballUp"].as(RADIOLIB_NC);
-            settingsMap[tbDownPin] = yamlConfig["Input"]["TrackballDown"].as(RADIOLIB_NC);
-            settingsMap[tbLeftPin] = yamlConfig["Input"]["TrackballLeft"].as(RADIOLIB_NC);
-            settingsMap[tbRightPin] = yamlConfig["Input"]["TrackballRight"].as(RADIOLIB_NC);
-            settingsMap[tbPressPin] = yamlConfig["Input"]["TrackballPress"].as(RADIOLIB_NC);
+            portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as("");
+            portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as("");
+
+            readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin);
+            readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin);
+            readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin);
+            readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin);
+            readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin);
+            readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin);
+
             if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") {
-                settingsMap[tbDirection] = 4;
+                portduino_config.tbDirection = 4;
             } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") {
-                settingsMap[tbDirection] = 3;
+                portduino_config.tbDirection = 3;
             }
         }
 
         if (yamlConfig["Webserver"]) {
-            settingsMap[webserverport] = (yamlConfig["Webserver"]["Port"]).as(-1);
-            settingsStrings[webserverrootpath] =
+            portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1);
+            portduino_config.webserver_root_path =
                 (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web");
-            settingsStrings[websslkeypath] =
+            portduino_config.webserver_ssl_key_path =
                 (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem");
-            settingsStrings[websslcertpath] =
+            portduino_config.webserver_ssl_cert_path =
                 (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem");
         }
 
         if (yamlConfig["HostMetrics"]) {
-            settingsMap[hostMetrics_channel] = (yamlConfig["HostMetrics"]["Channel"]).as(0);
-            settingsMap[hostMetrics_interval] = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0);
-            settingsStrings[hostMetrics_user_command] = (yamlConfig["HostMetrics"]["UserStringCommand"]).as("");
+            portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0);
+            portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0);
+            portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as("");
         }
 
         if (yamlConfig["Config"]) {
             if (yamlConfig["Config"]["DisplayMode"]) {
-                settingsMap[has_configDisplayMode] = true;
+                portduino_config.has_configDisplayMode = true;
                 if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") {
-                    settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR;
+                    portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR;
                 } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") {
-                    settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED;
+                    portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED;
                 } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") {
-                    settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
+                    portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
                 } else {
-                    settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
+                    portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
                 }
             }
         }
 
         if (yamlConfig["General"]) {
-            settingsMap[maxnodes] = (yamlConfig["General"]["MaxNodes"]).as(200);
-            settingsMap[maxtophone] = (yamlConfig["General"]["MaxMessageQueue"]).as(100);
-            settingsStrings[config_directory] = (yamlConfig["General"]["ConfigDirectory"]).as("");
-            settingsStrings[available_directory] =
+            portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200);
+            portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100);
+            portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as("");
+            portduino_config.available_directory =
                 (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/");
             if ((yamlConfig["General"]["MACAddress"]).as("") != "" &&
                 (yamlConfig["General"]["MACAddressSource"]).as("") != "") {
                 std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl;
                 exit(EXIT_FAILURE);
             }
-            settingsStrings[mac_address] = (yamlConfig["General"]["MACAddress"]).as("");
-            if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") {
-                std::ifstream infile("/sys/class/net/" + (yamlConfig["General"]["MACAddressSource"]).as("") +
-                                     "/address");
-                std::getline(infile, settingsStrings[mac_address]);
+            portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as("");
+            if (portduino_config.mac_address != "") {
+                portduino_config.mac_address_explicit = true;
+            } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") {
+                portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as("");
+                std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address");
+                std::getline(infile, portduino_config.mac_address);
             }
 
             // https://stackoverflow.com/a/20326454
-            settingsStrings[mac_address].erase(
-                std::remove(settingsStrings[mac_address].begin(), settingsStrings[mac_address].end(), ':'),
-                settingsStrings[mac_address].end());
+            portduino_config.mac_address.erase(
+                std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'),
+                portduino_config.mac_address.end());
         }
     } catch (YAML::Exception &e) {
         std::cout << "*** Exception " << e.what() << std::endl;
@@ -851,12 +730,12 @@ bool MAC_from_string(std::string mac_str, uint8_t *dmac)
 {
     mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end());
     if (mac_str.length() == 12) {
-        dmac[0] = std::stoi(settingsStrings[mac_address].substr(0, 2), nullptr, 16);
-        dmac[1] = std::stoi(settingsStrings[mac_address].substr(2, 2), nullptr, 16);
-        dmac[2] = std::stoi(settingsStrings[mac_address].substr(4, 2), nullptr, 16);
-        dmac[3] = std::stoi(settingsStrings[mac_address].substr(6, 2), nullptr, 16);
-        dmac[4] = std::stoi(settingsStrings[mac_address].substr(8, 2), nullptr, 16);
-        dmac[5] = std::stoi(settingsStrings[mac_address].substr(10, 2), nullptr, 16);
+        dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16);
+        dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16);
+        dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16);
+        dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16);
+        dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16);
+        dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16);
         return true;
     } else {
         return false;
@@ -875,4 +754,19 @@ std::string exec(const char *cmd)
         result += buffer.data();
     }
     return result;
+}
+
+void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault)
+{
+    if (sourceNode.IsMap()) {
+        destPin.enabled = true;
+        destPin.pin = sourceNode["pin"].as(pinDefault);
+        destPin.line = sourceNode["line"].as(destPin.pin);
+        destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip);
+    } else if (sourceNode) { // backwards API compatibility
+        destPin.enabled = true;
+        destPin.pin = sourceNode.as(pinDefault);
+        destPin.line = destPin.pin;
+        destPin.gpiochip = portduino_config.lora_default_gpiochip;
+    }
 }
\ No newline at end of file
diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h
index 8c36a1180..106900c48 100644
--- a/src/platform/portduino/PortduinoGlue.h
+++ b/src/platform/portduino/PortduinoGlue.h
@@ -6,6 +6,7 @@
 #include "LR11x0Interface.h"
 #include "Module.h"
 #include "platform/portduino/USBHal.h"
+#include "yaml-cpp/yaml.h"
 
 // Product strings for auto-configuration
 // {"PRODUCT_STRING", "CONFIG.YAML"}
@@ -19,36 +20,10 @@ inline const std::unordered_map configProducts = {
     {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"},
     {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}};
 
-enum configNames {
-    default_gpiochip,
-    cs_pin,
-    cs_line,
-    cs_gpiochip,
-    irq_pin,
-    irq_line,
-    irq_gpiochip,
-    busy_pin,
-    busy_line,
-    busy_gpiochip,
-    reset_pin,
-    reset_line,
-    reset_gpiochip,
-    txen_pin,
-    txen_line,
-    txen_gpiochip,
-    rxen_pin,
-    rxen_line,
-    rxen_gpiochip,
-    sx126x_ant_sw_pin,
-    sx126x_ant_sw_line,
-    sx126x_ant_sw_gpiochip,
-    sx126x_max_power,
-    sx128x_max_power,
-    lr1110_max_power,
-    lr1120_max_power,
-    rf95_max_power,
-    dio2_as_rf_switch,
-    dio3_tcxo_voltage,
+enum screen_modules { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d };
+enum touchscreen_modules { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 };
+enum portduino_log_level { level_error, level_warn, level_info, level_debug, level_trace };
+enum lora_module_enum {
     use_simradio,
     use_autoconf,
     use_rf95,
@@ -58,72 +33,18 @@ enum configNames {
     use_lr1110,
     use_lr1120,
     use_lr1121,
-    use_llcc68,
-    lora_usb_serial_num,
-    lora_usb_pid,
-    lora_usb_vid,
-    userButtonPin,
-    tbUpPin,
-    tbDownPin,
-    tbLeftPin,
-    tbRightPin,
-    tbPressPin,
-    tbDirection,
-    spidev,
-    spiSpeed,
-    i2cdev,
-    has_gps,
-    touchscreenModule,
-    touchscreenCS,
-    touchscreenIRQ,
-    touchscreenI2CAddr,
-    touchscreenBusFrequency,
-    touchscreenRotate,
-    touchscreenspidev,
-    displayspidev,
-    displayBusFrequency,
-    displayPanel,
-    displayWidth,
-    displayHeight,
-    displayCS,
-    displayDC,
-    displayRGBOrder,
-    displayBacklight,
-    displayBacklightPWMChannel,
-    displayBacklightInvert,
-    displayReset,
-    displayRotate,
-    displayOffsetRotate,
-    displayOffsetX,
-    displayOffsetY,
-    displayInvert,
-    keyboardDevice,
-    pointerDevice,
-    logoutputlevel,
-    traceFilename,
-    webserver,
-    webserverport,
-    webserverrootpath,
-    websslkeypath,
-    websslcertpath,
-    maxtophone,
-    maxnodes,
-    ascii_logs,
-    config_directory,
-    available_directory,
-    mac_address,
-    hostMetrics_interval,
-    hostMetrics_channel,
-    hostMetrics_user_command,
-    configDisplayMode,
-    has_configDisplayMode
+    use_llcc68
+};
+
+struct pinMapping {
+    std::string config_section;
+    std::string config_name;
+    int pin = RADIOLIB_NC;
+    int gpiochip;
+    int line;
+    bool enabled = false;
 };
-enum { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d };
-enum { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 };
-enum { level_error, level_warn, level_info, level_debug, level_trace };
 
-extern std::map settingsMap;
-extern std::map settingsStrings;
 extern std::ofstream traceFile;
 extern Ch341Hal *ch341Hal;
 int initGPIOPin(int pinNum, std::string gpioChipname, int line);
@@ -131,13 +52,422 @@ bool loadConfig(const char *configPath);
 static bool ends_with(std::string_view str, std::string_view suffix);
 void getMacAddr(uint8_t *dmac);
 bool MAC_from_string(std::string mac_str, uint8_t *dmac);
+void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault = RADIOLIB_NC);
 std::string exec(const char *cmd);
 
 extern struct portduino_config_struct {
+    // Lora
+    std::map loraModules = {
+        {use_simradio, "sim"},  {use_autoconf, "auto"}, {use_rf95, "RF95"},     {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"},
+        {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}};
+
+    std::map screen_names = {{x11, "X11"},         {fb, "FB"},           {st7789, "ST7789"},
+                                                          {st7735, "ST7735"},   {st7735s, "ST7735S"}, {st7796, "ST7796"},
+                                                          {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, {ili9486, "ILI9486"},
+                                                          {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}};
+
+    lora_module_enum lora_module;
     bool has_rfswitch_table = false;
     uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
     Module::RfSwitchMode_t rfswitch_table[8];
     bool force_simradio = false;
     bool has_device_id = false;
     uint8_t device_id[16] = {0};
+    std::string lora_spi_dev = "";
+    std::string lora_usb_serial_num = "";
+    int lora_spi_dev_int = 0;
+    int lora_default_gpiochip = 0;
+    int sx126x_max_power = 22;
+    int sx128x_max_power = 13;
+    int lr1110_max_power = 22;
+    int lr1120_max_power = 13;
+    int rf95_max_power = 20;
+    bool dio2_as_rf_switch = false;
+    int dio3_tcxo_voltage = 0;
+    int lora_usb_pid = 0x5512;
+    int lora_usb_vid = 0x1A86;
+    int spiSpeed = 2000000;
+    pinMapping lora_cs_pin = {"Lora", "CS"};
+    pinMapping lora_irq_pin = {"Lora", "IRQ"};
+    pinMapping lora_busy_pin = {"Lora", "Busy"};
+    pinMapping lora_reset_pin = {"Lora", "Reset"};
+    pinMapping lora_txen_pin = {"Lora", "TXen"};
+    pinMapping lora_rxen_pin = {"Lora", "RXen"};
+    pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"};
+
+    // GPS
+    bool has_gps = false;
+
+    // I2C
+    std::string i2cdev = "";
+
+    // Display
+    std::string display_spi_dev = "";
+    int display_spi_dev_int = 0;
+    int displayBusFrequency = 40000000;
+    screen_modules displayPanel = no_screen;
+    int displayWidth = 0;
+    int displayHeight = 0;
+    bool displayRGBOrder = false;
+    bool displayBacklightInvert = false;
+    bool displayRotate = false;
+    int displayOffsetRotate = 1;
+    bool displayInvert = false;
+    int displayOffsetX = 0;
+    int displayOffsetY = 0;
+    pinMapping displayDC = {"Display", "DC"};
+    pinMapping displayCS = {"Display", "CS"};
+    pinMapping displayBacklight = {"Display", "Backlight"};
+    pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"};
+    pinMapping displayReset = {"Display", "Reset"};
+
+    // Touchscreen
+    std::string touchscreen_spi_dev = "";
+    int touchscreen_spi_dev_int = 0;
+    touchscreen_modules touchscreenModule = no_touchscreen;
+    int touchscreenI2CAddr = -1;
+    int touchscreenBusFrequency = 1000000;
+    int touchscreenRotate = -1;
+    pinMapping touchscreenCS = {"Touchscreen", "CS"};
+    pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"};
+
+    // Input
+    std::string keyboardDevice = "";
+    std::string pointerDevice = "";
+    int tbDirection;
+    pinMapping userButtonPin = {"Input", "User"};
+    pinMapping tbUpPin = {"Input", "TrackballUp"};
+    pinMapping tbDownPin = {"Input", "TrackballDown"};
+    pinMapping tbLeftPin = {"Input", "TrackballLwft"};
+    pinMapping tbRightPin = {"Input", "TrackballRight"};
+    pinMapping tbPressPin = {"Input", "TrackballPress"};
+
+    // Logging
+    portduino_log_level logoutputlevel = level_debug;
+    std::string traceFilename;
+    bool ascii_logs = !isatty(1);
+    bool ascii_logs_explicit = false;
+
+    // Webserver
+    std::string webserver_root_path = "";
+    std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem";
+    std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem";
+    int webserverport = -1;
+
+    // HostMetrics
+    std::string hostMetrics_user_command = "";
+    int hostMetrics_interval = 0;
+    int hostMetrics_channel = 0;
+
+    // config
+    int configDisplayMode = 0;
+    bool has_configDisplayMode = false;
+
+    // General
+    std::string mac_address = "";
+    bool mac_address_explicit = false;
+    std::string mac_address_source = "";
+    std::string config_directory = "";
+    std::string available_directory = "/etc/meshtasticd/available.d/";
+    int maxtophone = 100;
+    int MaxNodes = 200;
+
+    pinMapping *all_pins[20] = {&lora_cs_pin,
+                                &lora_irq_pin,
+                                &lora_busy_pin,
+                                &lora_reset_pin,
+                                &lora_txen_pin,
+                                &lora_rxen_pin,
+                                &lora_sx126x_ant_sw_pin,
+                                &displayDC,
+                                &displayCS,
+                                &displayBacklight,
+                                &displayBacklightPWMChannel,
+                                &displayReset,
+                                &touchscreenCS,
+                                &touchscreenIRQ,
+                                &userButtonPin,
+                                &tbUpPin,
+                                &tbDownPin,
+                                &tbLeftPin,
+                                &tbRightPin,
+                                &tbPressPin};
+
+    std::string emit_yaml()
+    {
+        YAML::Emitter out;
+        out << YAML::BeginMap;
+
+        // Lora
+        out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap;
+        out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module];
+
+        for (auto lora_pin : all_pins) {
+            if (lora_pin->config_section == "Lora" && lora_pin->enabled) {
+                out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap;
+                out << YAML::Key << "pin" << YAML::Value << lora_pin->pin;
+                out << YAML::Key << "line" << YAML::Value << lora_pin->line;
+                out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip;
+                out << YAML::EndMap; // User
+            }
+        }
+
+        if (sx126x_max_power != 22)
+            out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power;
+        if (sx128x_max_power != 13)
+            out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power;
+        if (lr1110_max_power != 22)
+            out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power;
+        if (lr1120_max_power != 13)
+            out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power;
+        if (rf95_max_power != 20)
+            out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power;
+        out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch;
+        if (dio3_tcxo_voltage != 0)
+            out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << dio3_tcxo_voltage;
+        if (lora_usb_pid != 0x5512)
+            out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid;
+        if (lora_usb_vid != 0x1A86)
+            out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid;
+        if (lora_spi_dev != "")
+            out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev;
+        if (lora_usb_serial_num != "")
+            out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num;
+        out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed;
+        if (rfswitch_dio_pins[0] != RADIOLIB_NC) {
+            out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap;
+
+            out << YAML::Key << "pins";
+            out << YAML::Value << YAML::Flow << YAML::BeginSeq;
+
+            for (int i = 0; i < 5; i++) {
+                // set up the pin array first
+                if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5)
+                    out << "DIO5";
+                if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6)
+                    out << "DIO6";
+                if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7)
+                    out << "DIO7";
+                if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8)
+                    out << "DIO8";
+                if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10)
+                    out << "DIO10";
+            }
+            out << YAML::EndSeq;
+
+            for (int i = 0; i < 7; i++) {
+                switch (i) {
+                case 0:
+                    out << YAML::Key << "MODE_STBY";
+                    break;
+                case 1:
+                    out << YAML::Key << "MODE_RX";
+                    break;
+                case 2:
+                    out << YAML::Key << "MODE_TX";
+                    break;
+                case 3:
+                    out << YAML::Key << "MODE_TX_HP";
+                    break;
+                case 4:
+                    out << YAML::Key << "MODE_TX_HF";
+                    break;
+                case 5:
+                    out << YAML::Key << "MODE_GNSS";
+                    break;
+                case 6:
+                    out << YAML::Key << "MODE_WIFI";
+                    break;
+                }
+
+                out << YAML::Value << YAML::Flow << YAML::BeginSeq;
+                for (int j = 0; j < 5; j++) {
+                    if (rfswitch_table[i].values[j] == HIGH) {
+                        out << "HIGH";
+                    } else {
+                        out << "LOW";
+                    }
+                }
+                out << YAML::EndSeq;
+            }
+            out << YAML::EndMap; // rfswitch_table
+        }
+        out << YAML::EndMap; // Lora
+
+        if (i2cdev != "") {
+            out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap;
+            out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev;
+            out << YAML::EndMap; // I2C
+        }
+
+        // Display
+        if (displayPanel != no_screen) {
+            out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap;
+            for (auto &screen_name : screen_names) {
+                if (displayPanel == screen_name.first)
+                    out << YAML::Key << "Module" << YAML::Value << screen_name.second;
+            }
+            for (auto display_pin : all_pins) {
+                if (display_pin->config_section == "Display" && display_pin->enabled) {
+                    out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap;
+                    out << YAML::Key << "pin" << YAML::Value << display_pin->pin;
+                    out << YAML::Key << "line" << YAML::Value << display_pin->line;
+                    out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip;
+                    out << YAML::EndMap;
+                }
+            }
+            out << YAML::Key << "spidev" << YAML::Value << display_spi_dev;
+            out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency;
+            if (displayWidth)
+                out << YAML::Key << "Width" << YAML::Value << displayWidth;
+            if (displayHeight)
+                out << YAML::Key << "Height" << YAML::Value << displayHeight;
+            if (displayRGBOrder)
+                out << YAML::Key << "RGBOrder" << YAML::Value << true;
+            if (displayBacklightInvert)
+                out << YAML::Key << "BacklightInvert" << YAML::Value << true;
+            if (displayRotate)
+                out << YAML::Key << "Rotate" << YAML::Value << true;
+            if (displayInvert)
+                out << YAML::Key << "Invert" << YAML::Value << true;
+            if (displayOffsetX)
+                out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX;
+            if (displayOffsetY)
+                out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY;
+
+            out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate;
+
+            out << YAML::EndMap; // Display
+        }
+
+        // Touchscreen
+        if (touchscreen_spi_dev != "") {
+            out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap;
+            out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev;
+            out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency;
+            switch (touchscreenModule) {
+            case xpt2046:
+                out << YAML::Key << "Module" << YAML::Value << "XPT2046";
+            case stmpe610:
+                out << YAML::Key << "Module" << YAML::Value << "STMPE610";
+            case gt911:
+                out << YAML::Key << "Module" << YAML::Value << "GT911";
+            case ft5x06:
+                out << YAML::Key << "Module" << YAML::Value << "FT5x06";
+            }
+            for (auto touchscreen_pin : all_pins) {
+                if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) {
+                    out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap;
+                    out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin;
+                    out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line;
+                    out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip;
+                    out << YAML::EndMap;
+                }
+            }
+            if (touchscreenRotate != -1)
+                out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate;
+            if (touchscreenI2CAddr != -1)
+                out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr;
+            out << YAML::EndMap; // Touchscreen
+        }
+
+        // Input
+        out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap;
+        if (keyboardDevice != "")
+            out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice;
+        if (pointerDevice != "")
+            out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice;
+
+        for (auto input_pin : all_pins) {
+            if (input_pin->config_section == "Input" && input_pin->enabled) {
+                out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap;
+                out << YAML::Key << "pin" << YAML::Value << input_pin->pin;
+                out << YAML::Key << "line" << YAML::Value << input_pin->line;
+                out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip;
+                out << YAML::EndMap;
+            }
+        }
+        if (tbDirection == 3)
+            out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING";
+
+        out << YAML::EndMap; // Input
+
+        out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap;
+        out << YAML::Key << "LogLevel" << YAML::Value;
+        switch (logoutputlevel) {
+        case level_error:
+            out << "error";
+            break;
+        case level_warn:
+            out << "warn";
+            break;
+        case level_info:
+            out << "info";
+            break;
+        case level_debug:
+            out << "debug";
+            break;
+        case level_trace:
+            out << "trace";
+            break;
+        }
+        if (traceFilename != "")
+            out << YAML::Key << "TraceFile" << YAML::Value << traceFilename;
+        if (ascii_logs_explicit) {
+            out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs;
+        }
+        out << YAML::EndMap; // Logging
+
+        // Webserver
+        if (webserver_root_path != "") {
+            out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap;
+            out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path;
+            out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path;
+            out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path;
+            out << YAML::Key << "Port" << YAML::Value << webserverport;
+            out << YAML::EndMap; // Webserver
+        }
+
+        // HostMetrics
+        if (hostMetrics_user_command != "") {
+            out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap;
+            out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command;
+            out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval;
+            out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel;
+
+            out << YAML::EndMap; // HostMetrics
+        }
+
+        // config
+        if (has_configDisplayMode) {
+            out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap;
+            switch (configDisplayMode) {
+            case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR:
+                out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR";
+            case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED:
+                out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED";
+            case meshtastic_Config_DisplayConfig_DisplayMode_COLOR:
+                out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR";
+            case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT:
+                out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT";
+            }
+
+            out << YAML::EndMap; // Config
+        }
+
+        // General
+        out << YAML::Key << "General" << YAML::Value << YAML::BeginMap;
+        if (config_directory != "")
+            out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory;
+        if (mac_address_explicit)
+            out << YAML::Key << "MACAddress" << YAML::Value << mac_address;
+        if (mac_address_source != "")
+            out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source;
+        if (available_directory != "")
+            out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory;
+        out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone;
+        out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes;
+        out << YAML::EndMap; // General
+        return out.c_str();
+    }
 } portduino_config;
\ No newline at end of file
diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h
index 07d0aeee0..e10519d21 100644
--- a/src/platform/portduino/architecture.h
+++ b/src/platform/portduino/architecture.h
@@ -28,9 +28,9 @@
 #endif
 #ifndef HAS_TRACKBALL
 #define HAS_TRACKBALL 1
-#define TB_DOWN (uint8_t) settingsMap[tbDownPin]
-#define TB_UP (uint8_t) settingsMap[tbUpPin]
-#define TB_LEFT (uint8_t) settingsMap[tbLeftPin]
-#define TB_RIGHT (uint8_t) settingsMap[tbRightPin]
-#define TB_PRESS (uint8_t) settingsMap[tbPressPin]
+#define TB_DOWN (uint8_t) portduino_config.tbDownPin.pin
+#define TB_UP (uint8_t) portduino_config.tbUpPin.pin
+#define TB_LEFT (uint8_t) portduino_config.tbLeftPin.pin
+#define TB_RIGHT (uint8_t) portduino_config.tbRightPin.pin
+#define TB_PRESS (uint8_t) portduino_config.tbPressPin.pin
 #endif
\ No newline at end of file
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/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
index 24866654a..37cfc1626 100644
--- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp
+++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp
@@ -1,30 +1,7 @@
 #include "../test_helpers.h"
 
-// test data initialization
-const int from = 0x11223344;
-const int to = 0x55667788;
-const int id = 0x9999;
-
-// Helper function to create a test encrypted packet
-meshtastic_MeshPacket create_test_encrypted_packet(uint32_t from, uint32_t to, uint32_t id, const char *data)
-{
-    meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
-    packet.from = from;
-    packet.to = to;
-    packet.id = id;
-    packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
-
-    if (data) {
-        packet.encrypted.size = strlen(data);
-        memcpy(packet.encrypted.bytes, data, packet.encrypted.size);
-    }
-
-    return packet;
-}
-
-// Comprehensive helper function for all encrypted packet assertions
-void assert_encrypted_packet(const std::string &json, uint32_t expected_from, uint32_t expected_to, uint32_t expected_id,
-                             size_t expected_size)
+// Helper function for all encrypted packet assertions
+void assert_encrypted_packet(const std::string &json, meshtastic_MeshPacket packet)
 {
     // Parse and validate JSON
     TEST_ASSERT_TRUE(json.length() > 0);
@@ -37,24 +14,24 @@ void assert_encrypted_packet(const std::string &json, uint32_t expected_from, ui
 
     // Assert basic packet fields
     TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end());
-    TEST_ASSERT_EQUAL(expected_from, (uint32_t)jsonObj.at("from")->AsNumber());
+    TEST_ASSERT_EQUAL(packet.from, (uint32_t)jsonObj.at("from")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end());
-    TEST_ASSERT_EQUAL(expected_to, (uint32_t)jsonObj.at("to")->AsNumber());
+    TEST_ASSERT_EQUAL(packet.to, (uint32_t)jsonObj.at("to")->AsNumber());
 
     TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end());
-    TEST_ASSERT_EQUAL(expected_id, (uint32_t)jsonObj.at("id")->AsNumber());
+    TEST_ASSERT_EQUAL(packet.id, (uint32_t)jsonObj.at("id")->AsNumber());
 
     // Assert encrypted data fields
     TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end());
     TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString());
 
     TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end());
-    TEST_ASSERT_EQUAL(expected_size, (int)jsonObj.at("size")->AsNumber());
+    TEST_ASSERT_EQUAL(packet.encrypted.size, (int)jsonObj.at("size")->AsNumber());
 
     // Assert hex encoding
     std::string encrypted_hex = jsonObj["bytes"]->AsString();
-    TEST_ASSERT_EQUAL(expected_size * 2, encrypted_hex.length());
+    TEST_ASSERT_EQUAL(packet.encrypted.size * 2, encrypted_hex.length());
 
     delete root;
 }
@@ -63,20 +40,20 @@ void assert_encrypted_packet(const std::string &json, uint32_t expected_from, ui
 void test_encrypted_packet_serialization()
 {
     const char *data = "encrypted_payload_data";
-
-    meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, 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, from, to, id, strlen(data));
+    assert_encrypted_packet(json, packet);
 }
 
 // Test empty encrypted packet
 void test_empty_encrypted_packet()
 {
-    const char *data = "";
-
-    meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, data);
+    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, from, to, id, strlen(data));
+    assert_encrypted_packet(json, packet);
 }
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/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/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/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/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h
index b7b39d6e8..a453c3b71 100644
--- a/variants/native/portduino-buildroot/variant.h
+++ b/variants/native/portduino-buildroot/variant.h
@@ -1,5 +1,5 @@
 #define HAS_SCREEN 1
 #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_RX_TOPHONE portduino_config.maxtophone
+#define MAX_NUM_NODES portduino_config.MaxNodes
\ No newline at end of file
diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h
index a7ca865be..af05fcf8d 100644
--- a/variants/native/portduino/variant.h
+++ b/variants/native/portduino/variant.h
@@ -3,8 +3,8 @@
 #endif
 #define CANNED_MESSAGE_MODULE_ENABLE 1
 #define HAS_GPS 1
-#define MAX_RX_TOPHONE settingsMap[maxtophone]
-#define MAX_NUM_NODES settingsMap[maxnodes]
+#define MAX_RX_TOPHONE portduino_config.maxtophone
+#define MAX_NUM_NODES portduino_config.MaxNodes
 
 // RAK12002 RTC Module
-#define RV3028_RTC (uint8_t)0b1010010
\ No newline at end of file
+#define RV3028_RTC (uint8_t)0b1010010
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/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/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 a12b9f21c..7fc6c7cba 100644
--- a/variants/stm32/rak3172/platformio.ini
+++ b/variants/stm32/rak3172/platformio.ini
@@ -15,5 +15,5 @@ build_flags =
   -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 506675fa8..cbf8265d9 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
 [VERSION]  
 major = 2
 minor = 7
-build = 8
+build = 9