diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml index 616f51746..32cd45000 100644 --- a/.github/workflows/build_esp32.yml +++ b/.github/workflows/build_esp32.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build ESP32 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware.bin - ota-firmware-target: release/bleota.bin - artifact-paths: | + pio_platform: esp32 + pio_env: ${{ inputs.board }} + pio_target: build + ota_firmware_source: firmware.bin + ota_firmware_target: release/bleota.bin + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-esp32-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.bin release/*.elf - #include-web-ui: true - arch: esp32 diff --git a/.github/workflows/build_esp32_c3.yml b/.github/workflows/build_esp32_c3.yml index 1b6b832e9..161786f99 100644 --- a/.github/workflows/build_esp32_c3.yml +++ b/.github/workflows/build_esp32_c3.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-c3: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build ESP32-C3 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-c3.bin - ota-firmware-target: release/bleota-c3.bin - artifact-paths: | + pio_platform: esp32 + pio_env: ${{ inputs.board }} + pio_target: build + ota_firmware_source: firmware-c3.bin + ota_firmware_target: release/bleota-c3.bin + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-esp32c3-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.bin release/*.elf - #include-web-ui: true - arch: esp32c3 diff --git a/.github/workflows/build_esp32_c6.yml b/.github/workflows/build_esp32_c6.yml index 29dac51e1..90cdcc78e 100644 --- a/.github/workflows/build_esp32_c6.yml +++ b/.github/workflows/build_esp32_c6.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-c6: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build ESP32-C6 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-c3.bin - ota-firmware-target: release/bleota-c3.bin - artifact-paths: | + pio_platform: esp32 + pio_env: ${{ inputs.board }} + pio_target: build + ota_firmware_source: firmware-c3.bin + ota_firmware_target: release/bleota-c3.bin + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-esp32c6-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.bin release/*.elf - #include-web-ui: true - arch: esp32c6 diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml index 7e0373503..e5ed48e3e 100644 --- a/.github/workflows/build_esp32_s3.yml +++ b/.github/workflows/build_esp32_s3.yml @@ -11,27 +11,30 @@ permissions: read-all jobs: build-esp32-s3: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build ESP32-S3 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - remove-debug-flags: >- - ./arch/esp32/esp32.ini - ./arch/esp32/esp32s2.ini - ./arch/esp32/esp32s3.ini - ./arch/esp32/esp32c3.ini - ./arch/esp32/esp32c6.ini - build-script-path: bin/build-esp32.sh - ota-firmware-source: firmware-s3.bin - ota-firmware-target: release/bleota-s3.bin - artifact-paths: | + pio_platform: esp32 + pio_env: ${{ inputs.board }} + pio_target: build + ota_firmware_source: firmware-s3.bin + ota_firmware_target: release/bleota-s3.bin + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-esp32s3-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.bin release/*.elf - #include-web-ui: true - arch: esp32s3 diff --git a/.github/workflows/build_nrf52.yml b/.github/workflows/build_nrf52.yml index 786508f86..312aeb372 100644 --- a/.github/workflows/build_nrf52.yml +++ b/.github/workflows/build_nrf52.yml @@ -11,20 +11,30 @@ permissions: read-all jobs: build-nrf52: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build NRF52 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-nrf52.sh - artifact-paths: | - release/*.hex + pio_platform: nrf52 + pio_env: ${{ inputs.board }} + pio_target: build + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-nrf52840-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.uf2 release/*.elf - release/*.zip - arch: nrf52840 + release/*.hex + release/*-ota.zip diff --git a/.github/workflows/build_rpi2040.yml b/.github/workflows/build_rpi2040.yml index 53fee34d2..2abd7a839 100644 --- a/.github/workflows/build_rpi2040.yml +++ b/.github/workflows/build_rpi2040.yml @@ -11,18 +11,28 @@ permissions: read-all jobs: build-rpi2040: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build Raspberry Pi 2040 id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-rpi2040.sh - artifact-paths: | + pio_platform: rp2xx0 + pio_env: ${{ inputs.board }} + pio_target: build + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-rp2040-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.uf2 release/*.elf - arch: rp2040 diff --git a/.github/workflows/build_stm32.yml b/.github/workflows/build_stm32.yml index dc469d994..10680f422 100644 --- a/.github/workflows/build_stm32.yml +++ b/.github/workflows/build_stm32.yml @@ -11,19 +11,29 @@ permissions: read-all jobs: build-stm32: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - name: Get release version string + shell: bash + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + - name: Build STM32WL id: build - uses: ./.github/actions/build-variant + uses: meshtastic/gh-action-firmware@main with: - github_token: ${{ secrets.GITHUB_TOKEN }} - board: ${{ inputs.board }} - build-script-path: bin/build-stm32.sh - artifact-paths: | + pio_platform: stm32wl + pio_env: ${{ inputs.board }} + pio_target: build + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-stm32-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + overwrite: true + path: | release/*.hex release/*.bin release/*.elf - arch: stm32 diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 03e61d572..a676efa1e 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -135,6 +135,7 @@ jobs: board: ${{ matrix.board }} build-debian-src: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/build_debian_src.yml with: series: UNRELEASED @@ -425,7 +426,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-firmware: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware] env: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 36ec22f17..309772b12 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -8,6 +8,7 @@ permissions: read-all jobs: trunk_check: + if: github.repository == 'meshtastic/firmware' name: Trunk Check and Upload runs-on: ubuntu-24.04 @@ -21,6 +22,7 @@ jobs: trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk_upgrade: + if: github.repository == 'meshtastic/firmware' # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades name: Trunk Upgrade (PR) runs-on: ubuntu-24.04 diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d7eef29b4..e391aa07b 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -13,6 +13,7 @@ permissions: jobs: semgrep-full: + if: github.repository == 'meshtastic/firmware' runs-on: ubuntu-24.04 container: image: semgrep/semgrep diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 5ae6bdfc9..5a11fdfa8 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -11,6 +11,7 @@ permissions: jobs: stale_issues: + if: github.repository == 'meshtastic/firmware' name: Close Stale Issues runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28b6a40a5..34b28b39c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,9 +12,11 @@ permissions: jobs: native-tests: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/test_native.yml hardware-tests: + if: github.repository == 'meshtastic/firmware' runs-on: test-runner steps: - name: Checkout code diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 2ddebdf1d..0986e6eb0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,14 +9,14 @@ plugins: lint: enabled: - checkov@3.2.447 - - renovate@41.17.2 + - renovate@41.23.4 - prettier@3.6.2 - trufflehog@3.89.2 - yamllint@1.37.1 - - bandit@1.8.5 - - trivy@0.64.0 + - bandit@1.8.6 + - trivy@0.64.1 - taplo@0.9.3 - - ruff@0.12.1 + - ruff@0.12.2 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index a0030d223..9d21562a8 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -49,12 +49,12 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/896f1771ceb5979987a0b41028bf1b4e7aad419b.zip + https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=git-refs depName=XPowersLib packageName=lewisxhe/XPowersLib + # renovate: datasource=git-refs depName=XPowersLib packageName=https://github.com/lewisxhe/XPowersLib gitBranch=master https://github.com/lewisxhe/XPowersLib/archive/refs/tags/v0.3.0.zip # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip diff --git a/arch/esp32/esp32c6.ini b/arch/esp32/esp32c6.ini index 26b5c0f5b..1afb9b547 100644 --- a/arch/esp32/esp32c6.ini +++ b/arch/esp32/esp32c6.ini @@ -28,7 +28,7 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@^0.2.7 + lewisxhe/XPowersLib@0.3.0 # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto diff --git a/bin/build-rpi2040.sh b/bin/build-rp2xx0.sh similarity index 100% rename from bin/build-rpi2040.sh rename to bin/build-rp2xx0.sh diff --git a/bin/build-stm32.sh b/bin/build-stm32wl.sh similarity index 100% rename from bin/build-stm32.sh rename to bin/build-stm32wl.sh diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml index 1d85a4a3b..d37f6cf6a 100644 --- a/bin/config.d/display-waveshare-1-44.yaml +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -22,5 +22,5 @@ Input: TrackballLeft: 5 TrackballRight: 26 TrackballPress: 13 - + TrackballDirection: FALLING # User: 21 diff --git a/bin/device-install.sh b/bin/device-install.sh index 42d0c4089..4674113b6 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -7,12 +7,7 @@ MCU="" # Variant groups BIGDB_8MB=( - # Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. -if [[ $FILENAME == *"-tft-"* ]]; then - TFT_BUILD=true -fi - -# Extract BASENAME from %FILENAME% for later use.r-s3" + "picomputer-s3" "unphone" "seeed-sensecap-indicator" "crowpanel-esp32s3" diff --git a/bin/device-update.sh b/bin/device-update.sh index 6adfe4e0e..2a39cdef7 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -30,6 +30,18 @@ Flash image file to device, leave existing system intact." EOF } +# Check for --change-mode and remove it from arguments +NEW_ARGS="" +for arg in "$@"; do + if [ "$arg" = "--change-mode" ]; then + CHANGE_MODE=true + else + NEW_ARGS="$NEW_ARGS \"\$arg\"" + fi +done + +# Reset positional parameters to filtered list +eval set -- $NEW_ARGS while getopts ":hp:P:f:" opt; do case "${opt}" in @@ -43,9 +55,6 @@ while getopts ":hp:P:f:" opt; do ;; f) FILENAME=${OPTARG} ;; - --change-mode) - CHANGE_MODE=true - ;; *) echo "Invalid flag." show_help >&2 @@ -55,7 +64,7 @@ while getopts ":hp:P:f:" opt; do done shift "$((OPTIND-1))" -if [[ $CHANGE_MODE == true ]]; then +if [ "$CHANGE_MODE" = true ]; then $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status exit 0 fi diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index ed57386a3..47082718a 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.2 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 diff --git a/boards/heltec_mesh_node_t114.json b/boards/heltec_mesh_node_t114.json index d516c9701..eda0ac3df 100644 --- a/boards/heltec_mesh_node_t114.json +++ b/boards/heltec_mesh_node_t114.json @@ -10,7 +10,8 @@ "hwids": [ ["0x239A", "0x4405"], ["0x239A", "0x0029"], - ["0x239A", "0x002A"] + ["0x239A", "0x002A"], + ["0x2886", "0x1667"] ], "usb_product": "HT-n5262", "mcu": "nrf52840", diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 2be716e22..9e8870041 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -11,7 +11,8 @@ ["0x239A", "0x8029"], ["0x239A", "0x0029"], ["0x239A", "0x002A"], - ["0x239A", "0x802A"] + ["0x239A", "0x802A"], + ["0x2886", "0x0057"] ], "usb_product": "T1000-E-BOOT", "mcu": "nrf52840", diff --git a/debian/changelog b/debian/changelog index 70a01bab4..42488692b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.1.0) UNRELEASED; urgency=medium +meshtasticd (2.7.2.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -25,4 +25,7 @@ meshtasticd (2.7.1.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Fri, 27 Jun 2025 20:12:21 +0000 + [ ] + * GitHub Actions Automatic version bump + + -- Fri, 04 Jul 2025 11:58:01 +0000 diff --git a/platformio.ini b/platformio.ini index 795f86eb9..89720f0ad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -109,7 +109,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/4b7bf369adfa5a7bd419fa8293d21206576d52d0.zip + https://github.com/meshtastic/device-ui/archive/8c7092c73425adfda1aac8c6960df06cd85f6d92.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -129,7 +129,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library adafruit/Adafruit MCP9808 Library@2.0.2 # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library - adafruit/Adafruit INA260 Library@1.5.2 + adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor diff --git a/protobufs b/protobufs index 386fa53c1..584f0a3a3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 386fa53c1596c8dfc547521f08df107f4cb3a275 +Subproject commit 584f0a3a359103acf0bfce506c1b1fc32c639841 diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 142241c43..345c738d6 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1536,7 +1536,10 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s 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()); - perhapsSetRTC(RTCQualityGPS, t); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { + // Clear the GPS buffer if we got an invalid time + clearBuffer(); + } return true; } else return false; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 219a593e0..5054be3f0 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -105,7 +105,7 @@ void readFromRTC() * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { static uint32_t lastSetMsec = 0; uint32_t now = millis(); @@ -113,7 +113,7 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); - return false; + return RTCSetResultInvalidTime; } #endif @@ -184,9 +184,9 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) readFromRTC(); #endif - return true; + return RTCSetResultSuccess; } else { - return false; + return RTCSetResultNotSet; // RTC was already set with a higher quality time } } @@ -215,7 +215,7 @@ const char *RtcName(RTCQuality quality) * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -bool perhapsSetRTC(RTCQuality q, struct tm &t) +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) { /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 @@ -231,7 +231,7 @@ bool perhapsSetRTC(RTCQuality q, struct tm &t) // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); if (t.tm_year < 0 || t.tm_year >= 300) { // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); - return false; + return RTCSetResultInvalidTime; } else { return perhapsSetRTC(q, &tv); } diff --git a/src/gps/RTC.h b/src/gps/RTC.h index caa48dc06..96dec575b 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -22,13 +22,22 @@ enum RTCQuality { RTCQualityGPS = 4 }; +/// The RTC set result codes +/// Used to indicate the result of an attempt to set the RTC. +enum RTCSetResult { + RTCSetResultNotSet = 0, ///< RTC was set successfully + RTCSetResultSuccess = 1, ///< RTC was set successfully + RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch) + RTCSetResultError = 4 ///< An error occurred while setting the RTC +}; + RTCQuality getRTCQuality(); extern uint32_t lastSetFromPhoneNtpOrGps; /// If we haven't yet set our RTC this boot, set it from a GPS derived time -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); -bool perhapsSetRTC(RTCQuality q, struct tm &t); +RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); +RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t); /// Return a string name for the quality const char *RtcName(RTCQuality quality); diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 260df2a8d..3bd20feec 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -6,6 +6,10 @@ #include "main.h" #include +#ifdef GXEPD2_DRIVER_0 +#include "einkDetect.h" +#endif + /* The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini Previously, these macros were defined at the top of this file. @@ -174,9 +178,8 @@ bool EInkDisplay::connect() } } -#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \ - defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ - defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) +#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ + defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) { // Start HSPI hspi = new SPIClass(HSPI); @@ -232,6 +235,23 @@ bool EInkDisplay::connect() adafruitDisplay->init(); adafruitDisplay->setRotation(3); } +#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) + + // Detect display model, before starting SPI + EInkDetectionResult displayModel = detectEInk(); + + // Start HSPI + hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + + // Create GxEPD2 object + adafruitDisplay = new GxEPD2_Multi((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC, + PIN_EINK_RES, PIN_EINK_BUSY, *hspi); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(3); + #endif return true; diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 93be197b0..284337627 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -5,6 +5,10 @@ #include "GxEPD2_BW.h" #include +#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models +#include "GxEPD2Multi.h" +#endif + /** * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * @@ -63,8 +67,15 @@ class EInkDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; - // AdafruitGFX display object - instantiated in connect(), variant specific +#ifdef GXEPD2_DRIVER_0 + // AdafruitGFX display object - wrapper for multiple drivers + // Allows runtime detection of multiple displays + // Avoid this situation if possible! + GxEPD2_Multi *adafruitDisplay = NULL; +#else + // AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific GxEPD2_BW *adafruitDisplay = NULL; +#endif // If display uses HSPI #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ diff --git a/src/graphics/GxEPD2Multi.h b/src/graphics/GxEPD2Multi.h new file mode 100644 index 000000000..f3807c9de --- /dev/null +++ b/src/graphics/GxEPD2Multi.h @@ -0,0 +1,135 @@ +// Wrapper class for GxEPD2_BW + +// Generic signature at build-time, so that we can detect display model at run-time +// Workaround for issue of GxEPD2_BW objects not having a shared base class +// Only exposes methods which we are actually using + +template class GxEPD2_Multi +{ + public: + void drawPixel(int16_t x, int16_t y, uint16_t color) + { + if (which == 0) + driver0->drawPixel(x, y, color); + else + driver1->drawPixel(x, y, color); + } + + bool nextPage() + { + if (which == 0) + return driver0->nextPage(); + else + return driver1->nextPage(); + } + + void hibernate() + { + if (which == 0) + driver0->hibernate(); + else + driver1->hibernate(); + } + + void init(uint32_t serial_diag_bitrate = 0) + { + if (which == 0) + driver0->init(serial_diag_bitrate); + else + driver1->init(serial_diag_bitrate); + } + + void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false) + { + if (which == 0) + driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + else + driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); + } + + void setRotation(uint8_t x) + { + if (which == 0) + driver0->setRotation(x); + else + driver1->setRotation(x); + } + + void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + { + if (which == 0) + driver0->setPartialWindow(x, y, w, h); + else + driver1->setPartialWindow(x, y, w, h); + } + + void setFullWindow() + { + if (which == 0) + driver0->setFullWindow(); + else + driver1->setFullWindow(); + } + + int16_t width() + { + if (which == 0) + return driver0->width(); + else + return driver1->width(); + } + + int16_t height() + { + if (which == 0) + return driver0->height(); + else + return driver1->height(); + } + + void clearScreen(uint8_t value = 0xFF) + { + if (which == 0) + driver0->clearScreen(); + else + driver1->clearScreen(); + } + + void endAsyncFull() + { + if (which == 0) + driver0->endAsyncFull(); + else + driver1->endAsyncFull(); + } + + // Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd + class Epd2Wrapper + { + public: + bool isBusy() { return m_epd2->isBusy(); } + GxEPD2_EPD *m_epd2; + } epd2; + + // Constructor + // Select driver by passing whichDriver as 0 or 1 + GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi) + { + assert(whichDriver == 0 || whichDriver == 1); + which = whichDriver; + LOG_DEBUG("GxEPD2_Multi driver: %d", which); + + if (which == 0) { + driver0 = new GxEPD2_BW(Driver0(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver0->epd2); + } else if (which == 1) { + driver1 = new GxEPD2_BW(Driver1(cs, dc, rst, busy, spi)); + epd2.m_epd2 = &(driver1->epd2); + } + } + + private: + uint8_t which; + GxEPD2_BW *driver0; + GxEPD2_BW *driver1; +}; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 00994091a..5833265e7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -294,13 +294,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); int32_t rawRGB = uiconfig.screen_rgb_color; if (rawRGB > 0 && rawRGB <= 255255255) { - uint8_t r = (rawRGB >> 16) & 0xFF; - uint8_t g = (rawRGB >> 8) & 0xFF; - uint8_t b = rawRGB & 0xFF; - LOG_INFO("Values of r,g,b: %d, %d, %d", r, g, b); + uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF; + uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF; + uint8_t TFT_MESH_b = rawRGB & 0xFF; + LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); - if (r <= 255 && g <= 255 && b <= 255) { - TFT_MESH = COLOR565(r, g, b); + if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) { + TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); } } @@ -313,8 +313,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O ST7789_MISO, ST7789_SCK); #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); - static_cast(dispdev)->setRGB(TFT_MESH); #endif + static_cast(dispdev)->setRGB(TFT_MESH); #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -944,22 +944,6 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(digital_icon_clock); #endif - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; - - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - if (fsi.positions.firstFavorite == 255) - fsi.positions.firstFavorite = numframes; - fsi.positions.lastFavorite = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo; - indicatorIcons.push_back(icon_node); - } - } - #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!dismissedFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; @@ -969,7 +953,7 @@ void Screen::setFrames(FrameFocus focus) #endif // Beware of what changes you make in this code! - // We pass numfames into GetMeshModulesWithUIFrames() which is highly important! + // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! // Inside of that callback, goes over to MeshModule.cpp and we run // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr // entries until we're ready to start building the matching entries. @@ -998,6 +982,34 @@ void Screen::setFrames(FrameFocus focus) LOG_DEBUG("Added modules. numframes: %d", numframes); + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + // Temporary array to hold favorite node frames + std::vector favoriteFrames; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } + } + + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; + } + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // ✅ Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); @@ -1009,8 +1021,7 @@ void Screen::setFrames(FrameFocus focus) static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list - // just changed) + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) // Focus on a specific frame, in the frame set we just created switch (focus) { @@ -1373,6 +1384,8 @@ int Screen::handleInputEvent(const InputEvent *event) this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { menuHandler::nodeListMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { showPrevFrame(); diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 9f2422748..7cd876ac5 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -99,10 +99,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); - bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; - if (chargePercent == 100) { + bool isCharging = powerStatus->getIsCharging(); + bool usbPowered = powerStatus->getHasUSB(); + + if (chargePercent >= 100) { isCharging = false; } + if (chargePercent == 101) { + usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable + // plugged in + } + uint32_t now = millis(); #ifndef USE_EINK @@ -115,48 +122,63 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti bool useHorizontalBattery = (isHighResolution && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; + // === Battery Icons === - if (useHorizontalBattery) { - int batteryX = 2; - int batteryY = HEADER_OFFSET_Y + 3; - display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); - display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); - else { - display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); - display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); - int fillWidth = 14 * chargePercent / 100; - display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); + if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging + batteryX += 1; + batteryY += 2; + if (isHighResolution) { + display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); + batteryX += 20; // Icon + 1 pixel + } else { + display->drawXbm(batteryX, batteryY, 10, 8, imgUSB); + batteryX += 11; // Icon + 1 pixel } } else { - int batteryX = 1; - int batteryY = HEADER_OFFSET_Y + 1; + if (useHorizontalBattery) { + batteryX += 1; + batteryY += 2; + display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); + display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); + else { + display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); + display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); + int fillWidth = 14 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); + } + batteryX += 18; // Icon + 2 pixels + } else { #ifdef USE_EINK - batteryY += 2; + batteryY += 2; #endif - display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); - if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); - else { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); - int fillHeight = 8 * chargePercent / 100; - int fillY = batteryY - fillHeight; - display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + batteryX += 9; // Icon + 2 pixels } } - // === Battery % Display === - char chargeStr[4]; - snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); - int chargeNumWidth = display->getStringWidth(chargeStr); - const int batteryOffset = useHorizontalBattery ? 19 : 9; - const int percentX = x + batteryOffset; - display->drawString(percentX, textY, chargeStr); - display->drawString(percentX + chargeNumWidth - 1, textY, "%"); - if (isBold) { - display->drawString(percentX + 1, textY, chargeStr); - display->drawString(percentX + chargeNumWidth, textY, "%"); + if (chargePercent != 101) { + // === Battery % Display === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + display->drawString(batteryX, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(batteryX + 1, textY, chargeStr); + display->drawString(batteryX + chargeNumWidth, textY, "%"); + } } // === Time and Right-aligned Icons === diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 7ccb1c03c..8d7e91000 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -283,6 +283,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 xOffset += (isHighResolution) ? 32 : 18; } int yOffset = (isHighResolution) ? 3 : 1; +#ifdef SENSECAP_INDICATOR + yOffset -= 3; +#endif if (config.display.use_12h_clock) { display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, isPM ? "pm" : "am"); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 92954bf2e..c750b72c9 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -48,12 +48,14 @@ void menuHandler::LoraRegionPicker(uint32_t duration) "PH_433", "PH_868", "PH_915", - "ANZ_433"}; + "ANZ_433", + "KZ_433", + "KZ_863"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Set the LoRa region"; bannerOptions.durationMs = duration; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 23; + bannerOptions.optionsCount = 25; bannerOptions.InitialSelected = 0; bannerOptions.bannerCallback = [](int selected) -> void { if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { @@ -134,6 +136,7 @@ void menuHandler::ClockFacePicker() screen->setFrames(Screen::FOCUS_CLOCK); } }; + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; screen->showOverlayBanner(bannerOptions); } @@ -335,8 +338,8 @@ void menuHandler::homeBaseMenu() } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); } else if (selected == Bluetooth) { - InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + menuQueue = bluetooth_toggle_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -356,6 +359,9 @@ void menuHandler::systemBaseMenu() static int optionsEnumArray[7] = {Back}; int options = 1; + optionsArray[options] = "Reboot"; + optionsEnumArray[options++] = Reboot; + optionsArray[options] = "Beeps Action"; optionsEnumArray[options++] = Beeps; @@ -364,9 +370,6 @@ void menuHandler::systemBaseMenu() optionsEnumArray[options++] = Brightness; } - optionsArray[options] = "Reboot"; - optionsEnumArray[options++] = Reboot; - #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = Color; @@ -585,6 +588,23 @@ void menuHandler::GPSToggleMenu() } #endif +void menuHandler::BluetoothToggleMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Bluetooth"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1 || selected == 2) { + InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + }; + bannerOptions.InitialSelected = config.bluetooth.enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::BuzzerModeMenu() { static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"}; @@ -602,32 +622,28 @@ void menuHandler::BuzzerModeMenu() void menuHandler::BrightnessPickerMenu() { - static const char *optionsArray[] = {"Back", "Low", "Medium", "High", "Very High"}; + static const char *optionsArray[] = {"Back", "Low", "Medium", "High"}; // Get current brightness level to set initial selection - int currentSelection = 1; // Default to Low + int currentSelection = 1; // Default to Medium if (uiconfig.screen_brightness >= 255) { - currentSelection = 4; // Very High + currentSelection = 3; // Very High } else if (uiconfig.screen_brightness >= 128) { - currentSelection = 3; // High - } else if (uiconfig.screen_brightness >= 64) { - currentSelection = 2; // Medium + currentSelection = 2; // High } else { - currentSelection = 1; // Low + currentSelection = 1; // Medium } BannerOverlayOptions bannerOptions; bannerOptions.message = "Brightness"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; + bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { // Low - uiconfig.screen_brightness = 1; - } else if (selected == 2) { // Medium + if (selected == 1) { // Medium uiconfig.screen_brightness = 64; - } else if (selected == 3) { // High + } else if (selected == 2) { // High uiconfig.screen_brightness = 128; - } else if (selected == 4) { // Very High + } else if (selected == 3) { // Very High uiconfig.screen_brightness = 255; } @@ -679,52 +695,52 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 10; bannerOptions.bannerCallback = [display](int selected) -> void { - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; + uint8_t TFT_MESH_r = 0; + uint8_t TFT_MESH_g = 0; + uint8_t TFT_MESH_b = 0; if (selected == 1) { LOG_INFO("Setting color to system default or defined variant"); // Given just before we set all these to zero, we will allow this to go through } else if (selected == 2) { LOG_INFO("Setting color to Meshtastic Green"); - r = 103; - g = 234; - b = 148; + TFT_MESH_r = 103; + TFT_MESH_g = 234; + TFT_MESH_b = 148; } else if (selected == 3) { LOG_INFO("Setting color to Yellow"); - r = 255; - g = 255; - b = 128; + TFT_MESH_r = 255; + TFT_MESH_g = 255; + TFT_MESH_b = 128; } else if (selected == 4) { LOG_INFO("Setting color to Red"); - r = 255; - g = 64; - b = 64; + TFT_MESH_r = 255; + TFT_MESH_g = 64; + TFT_MESH_b = 64; } else if (selected == 5) { LOG_INFO("Setting color to Orange"); - r = 255; - g = 160; - b = 20; + TFT_MESH_r = 255; + TFT_MESH_g = 160; + TFT_MESH_b = 20; } else if (selected == 6) { LOG_INFO("Setting color to Purple"); - r = 204; - g = 153; - b = 255; + TFT_MESH_r = 204; + TFT_MESH_g = 153; + TFT_MESH_b = 255; } else if (selected == 7) { LOG_INFO("Setting color to Teal"); - r = 64; - g = 224; - b = 208; + TFT_MESH_r = 64; + TFT_MESH_g = 224; + TFT_MESH_b = 208; } else if (selected == 8) { LOG_INFO("Setting color to Pink"); - r = 255; - g = 105; - b = 180; + TFT_MESH_r = 255; + TFT_MESH_g = 105; + TFT_MESH_b = 180; } else if (selected == 9) { LOG_INFO("Setting color to White"); - r = 255; - g = 255; - b = 255; + TFT_MESH_r = 255; + TFT_MESH_g = 255; + TFT_MESH_b = 255; } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT @@ -733,14 +749,14 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->setColor(WHITE); - if (r == 0 && g == 0 && b == 0) { + if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif } else { - TFT_MESH = COLOR565(r, g, b); + TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) @@ -748,10 +764,10 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) #endif screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (r == 0 && g == 0 && b == 0) { + if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { uiconfig.screen_rgb_color = 0; } else { - uiconfig.screen_rgb_color = (r << 16) | (g << 8) | b; + uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; } LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uiconfig); @@ -833,6 +849,44 @@ void menuHandler::numberTest() [](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); }); } +void menuHandler::wifiBaseMenu() +{ + enum optionsNumbers { Back, Wifi_toggle }; + + static const char *optionsArray[] = {"Back", "WiFi Toggle"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "WiFi Menu"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::wifiToggleMenu() +{ + enum optionsNumbers { Back, Wifi_toggle }; + + static const char *optionsArray[] = {"Back", "Disable"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Disable Wifi and\nEnable Bluetooth?"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Wifi_toggle) { + config.network.wifi_enabled = false; + config.bluetooth.enabled = true; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != menu_none) @@ -896,6 +950,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case number_test: numberTest(); break; + case wifi_toggle_menu: + wifiToggleMenu(); + break; + case bluetooth_toggle_menu: + BluetoothToggleMenu(); + break; } menuQueue = menu_none; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 09279b041..d2169ca3c 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -13,9 +13,7 @@ class menuHandler clock_face_picker, clock_menu, position_base_menu, -#if !MESHTASTIC_EXCLUDE_GPS gps_toggle_menu, -#endif compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -26,7 +24,9 @@ class menuHandler add_favorite, remove_favorite, test_menu, - number_test + number_test, + wifi_toggle_menu, + bluetooth_toggle_menu }; static screenMenus menuQueue; @@ -54,6 +54,9 @@ class menuHandler static void removeFavoriteMenu(); static void testMenu(); static void numberTest(); + static void wifiBaseMenu(); + static void wifiToggleMenu(); + static void BluetoothToggleMenu(); }; } // namespace graphics \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3b682cc55..057c91008 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -42,7 +42,7 @@ uint32_t NotificationRenderer::currentNumber = 0; uint32_t pow_of_10(uint32_t n) { uint32_t ret = 1; - for (int i = 0; i < n; i++) { + for (uint32_t i = 0; i < n; i++) { ret *= 10; } return ret; @@ -80,6 +80,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU if (!isOverlayBannerShowing() || pauseBanner) return; switch (current_notification_type) { + case notificationTypeEnum::none: + // Do nothing - no notification to display + break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -144,12 +147,12 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation // copy the linestarts to display to the linePointers holder - for (int i = 0; i < lineCount; i++) { + for (uint16_t i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } std::string digits = " "; std::string arrowPointer = " "; - for (int i = 0; i < numDigits; i++) { + for (uint16_t i = 0; i < numDigits; i++) { // Modulo minus modulo to return just the current number digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " "; if (curSelected == i) { diff --git a/src/graphics/images.h b/src/graphics/images.h index c5865878a..beef3a1b2 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -12,7 +12,10 @@ const uint8_t imgSatellite[] PROGMEM = { 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000, }; -const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C}; +const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe, 0x85, 0xfe, 0x89, 0xff, 0xf1, 0xfc, 0x00, 0xfc}; +const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc, + 0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff, + 0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8}; const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C}; diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.cpp b/src/graphics/niche/Drivers/EInk/E0213A367.cpp new file mode 100644 index 000000000..f19cb4ff7 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/E0213A367.cpp @@ -0,0 +1,84 @@ +#include "./E0213A367.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void E0213A367::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +void E0213A367::configWaveform() +{ + // This command (0x37) is poorly documented + // As of July 2025, the datasheet for this display's controller IC is unavailable + // The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer + // Datasheet for the similar SSD1680 IC hints at the function of this command: + + // "Spare VCOM OTP selection": + // Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00. + // Maybe value is redundant? No noticeable impact when set to 0x00. + // We'll leave it set to 0x40, following Heltec's lead, just in case. + + // "Display Mode" + // Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh) + + // Unusual that waveforms are programmed to OTP, but this meta information is not ..? + + sendCommand(0x37); // "Write Register for Display Option" ? + sendData(0x40); // "Spare VCOM OTP selection" ? + sendData(0x80); // "Display Mode for WS[7:0]" ? + sendData(0x03); // "Display Mode for WS[15:8]" ? + sendData(0x0E); // "Display Mode [23:16]" ? + + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here. + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } +} + +// Tell controller IC which operations to run +void E0213A367::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh" + break; + } +} + +// Once the refresh operation has been started, +// begin periodically polling the display to check for completion, using the normal Meshtastic threading code +// Only used when refresh is "async" +void E0213A367::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh + } +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/E0213A367.h b/src/graphics/niche/Drivers/EInk/E0213A367.h new file mode 100644 index 000000000..a36fcb407 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/E0213A367.h @@ -0,0 +1,41 @@ +/* + +E-Ink display driver + - SSD1682 + - Manufacturer: SEEKINK + - Size: 2.13 inch + - Resolution: 122px x 255px + - Flex connector marking: HINK-E0213A162-A1 (hidden, printed on reverse) + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD1682.h" + +namespace NicheGraphics::Drivers +{ +class E0213A367 : public SSD1682 +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + E0213A367() : SSD1682(width, height, supported, 0) {} + + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.cpp b/src/graphics/niche/Drivers/EInk/SSD1682.cpp new file mode 100644 index 000000000..c3d7f7786 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD1682.cpp @@ -0,0 +1,41 @@ +#include "./SSD1682.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX) + : SSD16XX(width, height, supported, bufferOffsetX) +{ +} + +// SSD1682 only accepts single-byte x and y values +// This causes an incompatibility with the default SSD16XX::configFullscreen +void SSD1682::configFullscreen() +{ + // Define the boundaries of the "fullscreen" region, for the controller IC + static const uint8_t sx = bufferOffsetX; // Notice the offset + static const uint8_t sy = 0; + static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this + static const uint8_t ey = height; + + // Data entry mode - Left to Right, Top to Bottom + sendCommand(0x11); + sendData(0x03); + + // Select controller IC memory region to display a fullscreen image + sendCommand(0x44); // Memory X start - end + sendData(sx); + sendData(ex); + sendCommand(0x45); // Memory Y start - end + sendData(sy); + sendData(ey); + + // Place the cursor at the start of this memory region, ready to send image data x=0 y=0 + sendCommand(0x4E); // Memory cursor X + sendData(sx); + sendCommand(0x4F); // Memory cursor y + sendData(sy); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/SSD1682.h b/src/graphics/niche/Drivers/EInk/SSD1682.h new file mode 100644 index 000000000..ba3008537 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/SSD1682.h @@ -0,0 +1,31 @@ +/* + +E-Ink base class for displays based on SSD1682 + +SSD1682 has a few quirks. We're implementing them here in a new base class, +to avoid re-implementing them every time we need to add a new SSD1682-based display. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ + +class SSD1682 : public SSD16XX +{ + public: + SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0); + virtual void configFullscreen(); // Select memory region on controller IC + virtual void deepSleep() {} // Not usable (image memory not retained) +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index 858b1e132..ecaa7cea3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -52,6 +52,40 @@ void InkHUD::LogoApplet::onRender() setTextColor(WHITE); } +#ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc + + // Only show the custom screen at startup + // This allows us to draw the usual Meshtastic logo at shutdown + // The effect is similar to the two-stage userPrefs boot screen used by BaseUI + if (millis() < 10 * 1000UL) { + + // Draw the custom logo + const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA; + drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left + logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top + logo, // XBM data + USERPREFS_OEM_IMAGE_WIDTH, // Width + USERPREFS_OEM_IMAGE_HEIGHT, // Height + inverted ? WHITE : BLACK // Color + ); + + // Select the largest font which will still comfortably fit the custom text + setFont(fontLarge); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontMedium); + if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width()) + setFont(fontSmall); + + // Draw custom text below logo + int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo + printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP); + + // Don't draw the normal boot screen, we've already drawn our custom version + return; + } + +#endif + drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK); if (!textLeft.empty()) { diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 2397839b9..92db8720e 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -4,8 +4,13 @@ #include "mesh/NodeDB.h" #ifndef TB_DIRECTION +#if ARCH_PORTDUINO +#include "PortduinoGlue.h" +#define TB_DIRECTION (PinStatus) settingsMap[tbDirection] +#else #define TB_DIRECTION RISING #endif +#endif class TrackballInterruptBase : public Observable, public concurrency::OSThread { diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index bd82439fc..809e3c881 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -369,6 +369,14 @@ NodeDB::NodeDB() config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; } +#if !HAS_TFT + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // On a device without MUI, this display mode makes no sense, and will break logic. + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + config.bluetooth.enabled = true; + } +#endif + if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate))) saveWhat |= SEGMENT_DEVICESTATE; if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase))) diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index f42b151c8..8cac31a3e 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -246,8 +246,10 @@ void PacketHistory::insert(PacketRecord &r) #if RECENT_WARN_AGE > 0 if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { if (!(tu->id == r.id && tu->sender == r.sender)) { +#if VERBOSE_PACKET_HISTORY LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, RECENT_WARN_AGE / 1000); +#endif } else { // debug only #if VERBOSE_PACKET_HISTORY @@ -275,7 +277,9 @@ void PacketHistory::insert(PacketRecord &r) #endif if (r.rxTimeMsec == 0) { +#if VERBOSE_PACKET_HISTORY LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); +#endif return; // Return early if we can't update the history } diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 4db05b4d4..faa67a1c2 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -161,6 +161,14 @@ const RegionInfo regions[] = { RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false), RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false), + /* + Kazakhstan + 433.075 - 434.775 MHz <10 mW EIRP, Low Powered Devices (LPD) + 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields + https://github.com/meshtastic/firmware/issues/7204 + */ + RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ @@ -637,10 +645,6 @@ void RadioInterface::limitPower(int8_t loraMaxPower) if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; - if (TX_GAIN_LORA == 0) { // Setting power in config with defined TX_GAIN_LORA will cause decreasing power on each reboot - config.lora.tx_power = power; // Set limited power in config - } - LOG_INFO("Final Tx power: %d dBm", power); } @@ -681,4 +685,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); -} \ No newline at end of file +} diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index a650ba2fc..420d80e9a 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -10,6 +10,32 @@ #include "aes-ccm.h" #if !MESHTASTIC_EXCLUDE_PKI +/** + * Constant-time comparison of two byte arrays + * + * @param a First byte array to compare + * @param b Second byte array to compare + * @param len Number of bytes to compare + * @return 0 if arrays are equal, -1 if different or if inputs are invalid + */ +static int constant_time_compare(const void *a_, const void *b_, size_t len) +{ + /* Cast to volatile to prevent the compiler from optimizing out their comparison. */ + const volatile uint8_t *volatile a = (const volatile uint8_t *volatile)a_; + const volatile uint8_t *volatile b = (const volatile uint8_t *volatile)b_; + if (len == 0) + return 0; + if (a == NULL || b == NULL) + return -1; + size_t i; + volatile uint8_t d = 0U; + for (i = 0U; i < len; i++) { + d |= (a[i] ^ b[i]); + } + /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ + return (1 & ((d - 1) >> 8)) - 1; +} + static void WPA_PUT_BE16(uint8_t *a, uint16_t val) { a[0] = val >> 8; @@ -146,7 +172,7 @@ bool aes_ccm_ad(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t aes_ccm_encr(L, crypt, crypt_len, plain, a); aes_ccm_auth_start(M, L, nonce, aad, aad_len, crypt_len, x); aes_ccm_auth(plain, crypt_len, x); - if (memcmp(x, t, M) != 0) { // FIXME make const comp + if (constant_time_compare(x, t, M) != 0) { return false; } return true; diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index ed1849be8..f28daadbd 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -285,7 +285,11 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { /* Philippines 915mhz */ meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21, /* Australia / New Zealand 433MHz */ - meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 = 22 + meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 = 22, + /* Kazakhstan 433MHz */ + meshtastic_Config_LoRaConfig_RegionCode_KZ_433 = 23, + /* Kazakhstan 863MHz */ + meshtastic_Config_LoRaConfig_RegionCode_KZ_863 = 24 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -681,8 +685,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET -#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ANZ_433+1)) +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_KZ_863 +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_KZ_863+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 12a586cd7..0ba0e1164 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -630,6 +630,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) #if USERPREFS_EVENT_MODE // If we're in event mode, nobody is a Router or Repeater if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE || config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; } diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 8f53c9229..834184292 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -29,6 +29,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; size_t numBytes = 0; bool hasChecked = false; + bool phoneWants = false; protected: virtual int32_t runOnce() override @@ -38,10 +39,10 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread for (uint8_t i = 0; i < queue_size; i++) { handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length()); } - LOG_WARN("Queue_size %u", queue_size); + LOG_DEBUG("Queue_size %u", queue_size); queue_size = 0; } - if (hasChecked == false) { + if (hasChecked == false && phoneWants == true) { numBytes = getFromRadio(fromRadioBytes); hasChecked = true; } @@ -98,9 +99,12 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { virtual void onRead(NimBLECharacteristic *pCharacteristic) { - while (!bluetoothPhoneAPI->hasChecked) { + int tries = 0; + bluetoothPhoneAPI->phoneWants = true; + while (!bluetoothPhoneAPI->hasChecked && tries < 100) { bluetoothPhoneAPI->setIntervalFromNow(0); delay(20); + tries++; } std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes, @@ -111,6 +115,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks bluetoothPhoneAPI->setIntervalFromNow(0); bluetoothPhoneAPI->numBytes = 0; bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; } }; @@ -186,7 +191,12 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); if (bluetoothPhoneAPI) { + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); bluetoothPhoneAPI->close(); + bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; + bluetoothPhoneAPI->numBytes = 0; + bluetoothPhoneAPI->queue_size = 0; } } }; diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index f582a116d..49d1acb4c 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -642,6 +642,11 @@ bool loadConfig(const char *configPath) settingsMap[tbLeftPin] = yamlConfig["Input"]["TrackballLeft"].as(RADIOLIB_NC); settingsMap[tbRightPin] = yamlConfig["Input"]["TrackballRight"].as(RADIOLIB_NC); settingsMap[tbPressPin] = yamlConfig["Input"]["TrackballPress"].as(RADIOLIB_NC); + if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { + settingsMap[tbDirection] = 4; + } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { + settingsMap[tbDirection] = 3; + } } if (yamlConfig["Webserver"]) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 5795f0d8d..e404b7f1c 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -63,6 +63,7 @@ enum configNames { tbLeftPin, tbRightPin, tbPressPin, + tbDirection, spidev, spiSpeed, i2cdev, diff --git a/variants/heltec_vision_master_e213/einkDetect.h b/variants/heltec_vision_master_e213/einkDetect.h new file mode 100644 index 000000000..35140db60 --- /dev/null +++ b/variants/heltec_vision_master_e213/einkDetect.h @@ -0,0 +1,35 @@ +#pragma once + +#include "configuration.h" + +enum class EInkDetectionResult : uint8_t { + LCMEN213EFC1 = 0, // Initial version + E0213A367 = 1, // E213 PCB marked V1.1 (Mid 2025) +}; + +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin + + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH + + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); + + delay(10); + + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); + + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); + + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; +} \ No newline at end of file diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index 5f443e4da..6a75ad90d 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -18,16 +18,22 @@ // Shared NicheGraphics components // -------------------------------- +#include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" -// Button feedback -#include "buzz.h" +#include "buzz.h" // Button feedback +#include "einkDetect.h" // Detect display model at runtime void setupNicheGraphics() { using namespace NicheGraphics; + // Detect E-Ink Model + // ------------------- + + EInkDetectionResult displayModel = detectEInk(); + // SPI // ----------------------------- @@ -38,7 +44,13 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + Drivers::EInk *driver; + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + driver = new Drivers::E0213A367; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -51,7 +63,11 @@ void setupNicheGraphics() // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1 + inkhud->setDisplayResilience(15, 3); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index 34cebb6e3..028caaeff 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -7,7 +7,8 @@ build_flags = -Ivariants/heltec_vision_master_e213 -DHELTEC_VISION_MASTER_E213 -DUSE_EINK - -DEINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -DGXEPD2_DRIVER_0=GxEPD2_213_FC1 + -DGXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk @@ -16,7 +17,7 @@ build_flags = -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 diff --git a/variants/heltec_wireless_paper/einkDetect.h b/variants/heltec_wireless_paper/einkDetect.h new file mode 100644 index 000000000..93b3f86e3 --- /dev/null +++ b/variants/heltec_wireless_paper/einkDetect.h @@ -0,0 +1,35 @@ +#pragma once + +#include "configuration.h" + +enum class EInkDetectionResult : uint8_t { + LCMEN213EFC1 = 0, // V1.1 + E0213A367 = 1, // V1.1.1, V1.2 +}; + +EInkDetectionResult detectEInk() +{ + // Test 1: Logic of BUSY pin + + // Determines controller IC manufacturer + // Fitipower: busy when LOW + // Solomon Systech: busy when HIGH + + // Force display BUSY by holding reset pin active + pinMode(PIN_EINK_RES, OUTPUT); + digitalWrite(PIN_EINK_RES, LOW); + + delay(10); + + // Read whether pin is HIGH or LOW while busy + pinMode(PIN_EINK_BUSY, INPUT); + bool busyLogic = digitalRead(PIN_EINK_BUSY); + + // Test complete. Release pin + pinMode(PIN_EINK_RES, INPUT); + + if (busyLogic == LOW) + return EInkDetectionResult::LCMEN213EFC1; + else // busy HIGH + return EInkDetectionResult::E0213A367; +} \ No newline at end of file diff --git a/variants/heltec_wireless_paper/nicheGraphics.h b/variants/heltec_wireless_paper/nicheGraphics.h index cbf80bc5e..445b57714 100644 --- a/variants/heltec_wireless_paper/nicheGraphics.h +++ b/variants/heltec_wireless_paper/nicheGraphics.h @@ -18,13 +18,21 @@ // Shared NicheGraphics components // -------------------------------- +#include "graphics/niche/Drivers/EInk/E0213A367.h" #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" +#include "einkDetect.h" // Detect display model at runtime + void setupNicheGraphics() { using namespace NicheGraphics; + // Detect E-Ink Model + // ------------------- + + EInkDetectionResult displayModel = detectEInk(); + // SPI // ----------------------------- @@ -35,7 +43,13 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::LCMEN213EFC1; + Drivers::EInk *driver; + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 + driver = new Drivers::LCMEN213EFC1; + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + driver = new Drivers::E0213A367; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -48,7 +62,11 @@ void setupNicheGraphics() // Set how many FAST updates per FULL update // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(10, 1.5); + + if (displayModel == EInkDetectionResult::LCMEN213EFC1) // V1.1 (unmarked) + inkhud->setDisplayResilience(10, 1.5); + else if (displayModel == EInkDetectionResult::E0213A367) // V1.1.1, V1.2 + inkhud->setDisplayResilience(15, 3); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index ce5b5e533..790646056 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -7,7 +7,8 @@ build_flags = ${esp32s3_base.build_flags} -I variants/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER - -D EINK_DISPLAY_MODEL=GxEPD2_213_FC1 + -D GXEPD2_DRIVER_0=GxEPD2_213_FC1 + -D GXEPD2_DRIVER_1=GxEPD2_213_E0213A367 -D EINK_WIDTH=250 -D EINK_HEIGHT=122 -D USE_EINK @@ -17,7 +18,7 @@ build_flags = -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 diff --git a/version.properties b/version.properties index 3fe1aa385..69f2d6af5 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 1 +build = 2