diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh
index 10a2db0bd..86ab775f9 100644
--- a/.clusterfuzzlite/build.sh
+++ b/.clusterfuzzlite/build.sh
@@ -51,7 +51,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do
fuzzer=$(basename "$f" .cpp)
cp -f "$f" src/fuzzer.cpp
pio run -vvv --environment "$PIO_ENV"
- program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program"
+ program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/meshtasticd"
cp "$program" "$OUT/$fuzzer"
# Copy shared libraries used by the fuzzer.
diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml
index f7bf95f83..f79e4fdb5 100644
--- a/.github/actionlint.yaml
+++ b/.github/actionlint.yaml
@@ -2,4 +2,5 @@
self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels:
+ - arctastic
- test-runner
diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml
index a71ddfc4d..c048b7ac2 100644
--- a/.github/actions/build-variant/action.yml
+++ b/.github/actions/build-variant/action.yml
@@ -76,7 +76,7 @@ runs:
done
- name: PlatformIO ${{ inputs.arch }} download cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.platformio/.cache
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
@@ -100,9 +100,9 @@ runs:
id: version
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
- name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
+ name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
overwrite: true
path: |
${{ inputs.artifact-paths }}
diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml
index d7d26f0e8..de114be1c 100644
--- a/.github/workflows/build_debian_src.yml
+++ b/.github/workflows/build_debian_src.yml
@@ -64,7 +64,7 @@ jobs:
PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
overwrite: true
diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml
index 9ac84c23e..d384540a4 100644
--- a/.github/workflows/build_firmware.yml
+++ b/.github/workflows/build_firmware.yml
@@ -18,7 +18,8 @@ permissions: read-all
jobs:
pio-build:
name: build-${{ inputs.platform }}
- runs-on: ubuntu-24.04
+ # Use 'arctastic' self-hosted runner pool when building in the main repo
+ runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps:
@@ -55,15 +56,31 @@ jobs:
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
+ - name: Job summary
+ env:
+ PIO_ENV: ${{ inputs.pio_env }}
+ run: |
+ echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
+ echo "Manifest
" >> $GITHUB_STEP_SUMMARY
+ echo '' >> $GITHUB_STEP_SUMMARY
+ echo '```json' >> $GITHUB_STEP_SUMMARY
+ cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
+ echo '' >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
+
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
id: upload
with:
- name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
+ name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
overwrite: true
path: |
+ release/*.mt.json
release/*.bin
release/*.elf
release/*.uf2
release/*.hex
- release/*-ota.zip
+ release/*.zip
+ release/device-*.sh
+ release/device-*.bat
diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml
deleted file mode 100644
index 5673f8cb6..000000000
--- a/.github/workflows/build_one_arch.yml
+++ /dev/null
@@ -1,176 +0,0 @@
-name: Build One Arch
-
-on:
- workflow_dispatch:
- inputs:
- # trunk-ignore(checkov/CKV_GHA_7)
- arch:
- type: choice
- options:
- - esp32
- - esp32s3
- - esp32c3
- - esp32c6
- - nrf52840
- - rp2040
- - rp2350
- - stm32
- - native
-
-permissions: read-all
-
-env:
- INPUT_ARCH: ${{ github.event.inputs.arch }}
-
-jobs:
- setup:
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-python@v6
- with:
- python-version: 3.x
- cache: pip
- - run: pip install -U platformio
- - name: Generate matrix
- id: jsonStep
- run: |
- TARGETS=$(./bin/generate_ci_matrix.py $INPUT_ARCH --level extra)
- echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
- echo "selected_arch=$TARGETS" >> $GITHUB_OUTPUT
- outputs:
- selected_arch: ${{ steps.jsonStep.outputs.selected_arch }}
-
- version:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v6
- - name: Get release version string
- run: |
- echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
- echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
- id: version
- env:
- BUILD_LOCATION: local
- outputs:
- long: ${{ steps.version.outputs.long }}
- deb: ${{ steps.version.outputs.deb }}
-
- build:
- if: ${{ github.event_name != 'workflow_dispatch' }}
- needs: [setup, version]
- strategy:
- fail-fast: false
- matrix:
- build: ${{ fromJson(needs.setup.outputs.selected_arch) }}
- uses: ./.github/workflows/build_firmware.yml
- with:
- version: ${{ needs.version.outputs.long }}
- pio_env: ${{ matrix.build.board }}
- platform: ${{ matrix.build.arch }}
-
- build-debian-src:
- if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
- uses: ./.github/workflows/build_debian_src.yml
- with:
- series: UNRELEASED
- build_location: local
- secrets: inherit
-
- package-pio-deps-native-tft:
- if: ${{ inputs.arch == 'native' }}
- uses: ./.github/workflows/package_pio_deps.yml
- with:
- pio_env: native-tft
- secrets: inherit
-
- test-native:
- if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }}
- uses: ./.github/workflows/test_native.yml
-
- gather-artifacts:
- permissions:
- contents: write
- pull-requests: write
- strategy:
- fail-fast: false
- matrix:
- arch:
- - esp32
- - esp32s3
- - esp32c3
- - esp32c6
- - nrf52840
- - rp2040
- - rp2350
- - stm32
- runs-on: ubuntu-latest
- needs: [version, build]
- steps:
- - name: Checkout code
- uses: actions/checkout@v6
- with:
- ref: ${{github.event.pull_request.head.ref}}
- repository: ${{github.event.pull_request.head.repo.full_name}}
-
- - uses: actions/download-artifact@v6
- with:
- path: ./
- pattern: firmware-${{inputs.arch}}-*
- merge-multiple: true
-
- - name: Display structure of downloaded files
- run: ls -R
-
- - name: Move files up
- run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
-
- - name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
- with:
- name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
- overwrite: true
- path: |
- ./firmware-*.bin
- ./firmware-*.uf2
- ./firmware-*.hex
- ./firmware-*-ota.zip
- ./device-*.sh
- ./device-*.bat
- ./littlefs-*.bin
- ./bleota*bin
- ./Meshtastic_nRF52_factory_erase*.uf2
- retention-days: 30
-
- - uses: actions/download-artifact@v6
- with:
- name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
- merge-multiple: true
- path: ./output
-
- # For diagnostics
- - name: Show artifacts
- run: ls -lR
-
- - name: Device scripts permissions
- run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
-
- - name: Zip firmware
- run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
-
- - name: Repackage in single elfs zip
- uses: actions/upload-artifact@v5
- with:
- name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
- overwrite: true
- path: ./*.elf
- retention-days: 30
-
- - uses: scruplelesswizard/comment-artifact@main
- if: ${{ github.event_name == 'pull_request' }}
- with:
- name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
- description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
- github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml
index 343e5be64..9cc0bac78 100644
--- a/.github/workflows/build_one_target.yml
+++ b/.github/workflows/build_one_target.yml
@@ -15,7 +15,6 @@ on:
- rp2040
- rp2350
- stm32
- - native
target:
type: string
required: false
@@ -42,7 +41,6 @@ jobs:
- rp2040
- rp2350
- stm32
-
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
@@ -60,7 +58,7 @@ jobs:
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
echo "Targets:" >> $GITHUB_STEP_SUMMARY
- echo $TARGETS >> $GITHUB_STEP_SUMMARY
+ echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY
version:
if: ${{ inputs.target != '' }}
@@ -87,25 +85,6 @@ jobs:
pio_env: ${{ inputs.target }}
platform: ${{ inputs.arch }}
- build-debian-src:
- if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }}
- uses: ./.github/workflows/build_debian_src.yml
- with:
- series: UNRELEASED
- build_location: local
- secrets: inherit
-
- package-pio-deps-native-tft:
- if: ${{ inputs.arch == 'native' }}
- uses: ./.github/workflows/package_pio_deps.yml
- with:
- pio_env: native-tft
- secrets: inherit
-
- test-native:
- if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }}
- uses: ./.github/workflows/test_native.yml
-
gather-artifacts:
permissions:
contents: write
@@ -119,7 +98,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-*-*
@@ -132,7 +111,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -140,7 +119,7 @@ jobs:
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
- ./firmware-*-ota.zip
+ ./firmware-*.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
@@ -148,7 +127,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -160,14 +139,14 @@ jobs:
- name: Device scripts permissions
run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
+ chmod +x ./output/device-install.sh || true
+ chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true
diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml
index 38373a2fc..d7bde7bc5 100644
--- a/.github/workflows/main_matrix.yml
+++ b/.github/workflows/main_matrix.yml
@@ -77,16 +77,21 @@ jobs:
fail-fast: false
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
-
- runs-on: ubuntu-latest
+ # Use 'arctastic' self-hosted runner pool when checking in the main repo
+ runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps:
- uses: actions/checkout@v6
- - name: Build base
- id: base
- uses: ./.github/actions/setup-base
+ with:
+ submodules: recursive
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }}
- run: bin/check-all.sh ${{ matrix.check.board }}
+ uses: meshtastic/gh-action-firmware@main
+ with:
+ pio_platform: ${{ matrix.check.platform }}
+ pio_env: ${{ matrix.check.board }}
+ pio_target: check
build:
needs: [setup, version]
@@ -168,7 +173,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -177,19 +182,17 @@ jobs:
- name: Display structure of downloaded files
run: ls -R
- - name: Move files up
- run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
-
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
+ ./firmware-*.mt.json
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
- ./firmware-*-ota.zip
+ ./firmware-*.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
@@ -197,7 +200,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -209,16 +212,16 @@ jobs:
- name: Device scripts permissions
run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
+ chmod +x ./output/device-install.sh || true
+ chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
- name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
+ name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: ./*.elf
retention-days: 30
@@ -236,6 +239,7 @@ jobs:
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
+ - setup
- version
- gather-artifacts
- build-debian-src
@@ -244,11 +248,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- - name: Setup Python
- uses: actions/setup-python@v6
- with:
- python-version: 3.x
-
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
@@ -261,14 +260,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -284,10 +283,25 @@ jobs:
- name: Display structure of downloaded files
run: ls -lR
- - name: Add Linux sources to GtiHub Release
+ - name: Generate Release manifest
+ run: |
+ jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{
+ "version": $ver,
+ "targets": $targets
+ }' > firmware-${{ needs.version.outputs.long }}.json
+
+ - name: Save Release manifest artifact
+ uses: actions/upload-artifact@v6
+ with:
+ name: manifest-${{ needs.version.outputs.long }}
+ overwrite: true
+ path: firmware-${{ needs.version.outputs.long }}.json
+
+ - name: Add sources to GitHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
+ gh release upload v${{ needs.version.outputs.long }} ./firmware-${{ needs.version.outputs.long }}.json
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
env:
@@ -318,7 +332,7 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -329,15 +343,15 @@ jobs:
- name: Device scripts permissions
run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
+ chmod +x ./output/device-install.sh || true
+ chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
- name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
+ name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./elfs
@@ -373,12 +387,19 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - name: Get firmware artifacts
+ uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
+ - name: Get manifest artifact
+ uses: actions/download-artifact@v7
+ with:
+ pattern: manifest-${{ needs.version.outputs.long }}
+ path: ./publish
+
- name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4
env:
diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml
index 154b230c7..bd3f6d4eb 100644
--- a/.github/workflows/merge_queue.yml
+++ b/.github/workflows/merge_queue.yml
@@ -147,7 +147,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -160,7 +160,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -168,7 +168,7 @@ jobs:
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
- ./firmware-*-ota.zip
+ ./firmware-*.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
@@ -176,7 +176,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -188,16 +188,16 @@ jobs:
- name: Device scripts permissions
run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
+ chmod +x ./output/device-install.sh || true
+ chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
- name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
+ name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: ./*.elf
retention-days: 30
@@ -223,11 +223,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- - name: Setup Python
- uses: actions/setup-python@v6
- with:
- python-version: 3.x
-
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
@@ -240,14 +235,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -297,7 +292,7 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -308,15 +303,15 @@ jobs:
- name: Device scripts permissions
run: |
- chmod +x ./output/device-install.sh
- chmod +x ./output/device-update.sh
+ chmod +x ./output/device-install.sh || true
+ chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
- name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
+ name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./elfs
@@ -352,7 +347,7 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml
index 2b202ed95..63f1fe8a0 100644
--- a/.github/workflows/package_obs.yml
+++ b/.github/workflows/package_obs.yml
@@ -58,7 +58,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml
index cb10a79f3..82ffe66e9 100644
--- a/.github/workflows/package_pio_deps.yml
+++ b/.github/workflows/package_pio_deps.yml
@@ -56,7 +56,7 @@ jobs:
PLATFORMIO_CORE_DIR: pio/core
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
overwrite: true
diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml
index 2e3278041..9a463dbea 100644
--- a/.github/workflows/package_ppa.yml
+++ b/.github/workflows/package_ppa.yml
@@ -60,7 +60,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml
index 048186538..6306d777f 100644
--- a/.github/workflows/pr_tests.yml
+++ b/.github/workflows/pr_tests.yml
@@ -50,9 +50,9 @@ jobs:
- name: Download test artifacts
if: needs.native-tests.result != 'skipped'
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
- name: platformio-test-report-${{ steps.version.outputs.long }}.zip
+ name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
- name: Parse test results and create detailed summary
diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml
index f21b13ee1..badbb31d4 100644
--- a/.github/workflows/release_channels.yml
+++ b/.github/workflows/release_channels.yml
@@ -102,7 +102,7 @@ jobs:
PIP_DISABLE_PIP_VERSION_CHECK: 1
- name: Create Bumps pull request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@v8
with:
base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version
diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml
index d044f9038..d93449d6d 100644
--- a/.github/workflows/sec_sast_semgrep_cron.yml
+++ b/.github/workflows/sec_sast_semgrep_cron.yml
@@ -33,7 +33,7 @@ jobs:
# step 3
- name: save report as pipeline artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: report.sarif
overwrite: true
diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml
index decd23954..cabe0dd97 100644
--- a/.github/workflows/test_native.yml
+++ b/.github/workflows/test_native.yml
@@ -40,7 +40,7 @@ jobs:
- name: Integration test
run: |
- .pio/build/coverage/program -s &
+ .pio/build/coverage/meshtasticd -s &
PID=$!
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
echo "Simulator started, launching python test..."
@@ -59,10 +59,10 @@ jobs:
id: version
- name: Save coverage information
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
- name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}.zip
+ name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
overwrite: true
path: ./coverage_*.info
@@ -94,9 +94,9 @@ jobs:
- name: Save test results
if: always() # run this step even if previous step failed
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
- name: platformio-test-report-${{ steps.version.outputs.long }}.zip
+ name: platformio-test-report-${{ steps.version.outputs.long }}
overwrite: true
path: ./testreport.xml
@@ -108,10 +108,10 @@ jobs:
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
- name: Save coverage information
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
- name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}.zip
+ name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
overwrite: true
path: ./coverage_*.info
@@ -137,9 +137,9 @@ jobs:
id: version
- name: Download test artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
- name: platformio-test-report-${{ steps.version.outputs.long }}.zip
+ name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
- name: Test Report
@@ -150,9 +150,9 @@ jobs:
reporter: java-junit
- name: Download coverage artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
- pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
+ pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
path: code-coverage-report
merge-multiple: true
@@ -163,7 +163,7 @@ jobs:
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
- name: Save Code Coverage Report
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
- name: code-coverage-report-${{ steps.version.outputs.long }}.zip
+ name: code-coverage-report-${{ steps.version.outputs.long }}
path: code-coverage-report
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4a97853e2..241f2cd10 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -22,7 +22,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6
- # - uses: actions/setup-python@v5
+ # - uses: actions/setup-python@v6
# with:
# python-version: '3.10'
diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml
index af0557fda..d9ef98194 100644
--- a/.github/workflows/update_protobufs.yml
+++ b/.github/workflows/update_protobufs.yml
@@ -31,7 +31,7 @@ jobs:
./bin/regen-protos.sh
- name: Create pull request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@v8
with:
branch: create-pull-request/update-protobufs
labels: submodules
diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index 95e5b0dd2..2d534189c 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -8,25 +8,25 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- - checkov@3.2.495
- - renovate@42.30.4
+ - checkov@3.2.496
+ - renovate@42.66.11
- prettier@3.7.4
- - trufflehog@3.91.2
+ - trufflehog@3.92.4
- yamllint@1.37.1
- bandit@1.9.2
- - trivy@0.67.2
+ - trivy@0.68.2
- taplo@0.10.0
- - ruff@0.14.7
+ - ruff@0.14.10
- isort@7.0.0
- - markdownlint@0.46.0
- - oxipng@9.1.5
+ - markdownlint@0.47.0
+ - oxipng@10.0.0
- svgo@4.0.0
- actionlint@1.7.9
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- - black@25.11.0
+ - black@25.12.0
- git-diff-check
- gitleaks@8.30.0
- clang-format@16.0.3
diff --git a/arch/esp32/esp32s3.ini b/arch/esp32/esp32s3.ini
deleted file mode 100644
index 8d8b6899e..000000000
--- a/arch/esp32/esp32s3.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[esp32s3_base]
-extends = esp32_base
-custom_esp32_kind = esp32s3
-
-monitor_speed = 115200
diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh
index 92836db23..4e799b30a 100755
--- a/bin/build-esp32.sh
+++ b/bin/build-esp32.sh
@@ -5,7 +5,8 @@ set -e
VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short`
-OUTDIR=release/
+BUILDDIR=.pio/build/$1
+OUTDIR=release
rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true
@@ -14,33 +15,27 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
-rm -f .pio/build/$1/firmware.*
+rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find
export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
-SRCELF=.pio/build/$1/firmware.elf
-cp $SRCELF $OUTDIR/$basename.elf
+pio run --environment $1 -t mtjson # -v
+
+cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying ESP32 bin file"
-SRCBIN=.pio/build/$1/firmware.factory.bin
-cp $SRCBIN $OUTDIR/$basename.bin
+cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
echo "Copying ESP32 update bin file"
-SRCBIN=.pio/build/$1/firmware.bin
-cp $SRCBIN $OUTDIR/$basename-update.bin
+cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
-echo "Building Filesystem for ESP32 targets"
-# If you want to build the webui, uncomment the following lines
-# pio run --environment $1 -t buildfs
-# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
-# # Remove webserver files from the filesystem and rebuild
-# ls -l data/static # Diagnostic list of files
-# rm -rf data/static
-pio run --environment $1 -t buildfs
-cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
-cp bin/device-install.* $OUTDIR
-cp bin/device-update.* $OUTDIR
\ No newline at end of file
+echo "Copying Filesystem for ESP32 targets"
+cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
+cp bin/device-install.* $OUTDIR/
+cp bin/device-update.* $OUTDIR/
+
+echo "Copying manifest"
+cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-native.sh b/bin/build-native.sh
index fff86e87e..f35e46a87 100755
--- a/bin/build-native.sh
+++ b/bin/build-native.sh
@@ -17,15 +17,19 @@ VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short)
PIO_ENV=${1:-native}
-OUTDIR=release/
+BUILDDIR=.pio/build/$PIO_ENV
+OUTDIR=release
-rm -f $OUTDIR/firmware*
+rm -f $OUTDIR/meshtasticd*
mkdir -p $OUTDIR/
rm -r $OUTDIR/* || true
+basename=meshtasticd-$1-$VERSION
+
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
pio pkg install --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed
-cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
-cp bin/native-install.* $OUTDIR
+
+cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
+cp bin/native-install.* $OUTDIR/
diff --git a/bin/build-nrf52.sh b/bin/build-nrf52.sh
index deca209d2..edcc2add2 100755
--- a/bin/build-nrf52.sh
+++ b/bin/build-nrf52.sh
@@ -5,7 +5,8 @@ set -e
VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short)
-OUTDIR=release/
+BUILDDIR=.pio/build/$1
+OUTDIR=release
rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true
@@ -14,40 +15,38 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
-rm -f .pio/build/$1/firmware.*
+rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find
export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
+ota_basename=${basename}-ota
-pio run --environment $1 # -v
-SRCELF=.pio/build/$1/firmware.elf
-cp $SRCELF $OUTDIR/$basename.elf
+pio run --environment $1 -t mtjson # -v
-echo "Generating NRF52 dfu file"
-DFUPKG=.pio/build/$1/firmware.zip
-cp $DFUPKG $OUTDIR/$basename-ota.zip
+cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
-echo "Generating NRF52 uf2 file"
-SRCHEX=.pio/build/$1/firmware.hex
+echo "Copying NRF52 dfu (OTA) file"
+cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip
-# if WM1110 target, merge hex with softdevice 7.3.0
+echo "Copying NRF52 UF2 file"
+cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
+cp bin/*.uf2 $OUTDIR/
+
+SRCHEX=$BUILDDIR/$basename.hex
+
+# if WM1110 target, copy the merged.hex
if (echo $1 | grep -q "wio-sdk-wm1110"); then
- echo "Merging with softdevice"
- bin/mergehex -m bin/s140_nrf52_7.3.0_softdevice.hex $SRCHEX -o .pio/build/$1/$basename.hex
- SRCHEX=.pio/build/$1/$basename.hex
- bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
- cp $SRCHEX $OUTDIR
- cp bin/*.uf2 $OUTDIR
-else
- bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
- cp bin/device-install.* $OUTDIR
- cp bin/device-update.* $OUTDIR
- cp bin/*.uf2 $OUTDIR
+ echo "Copying .merged.hex file"
+ SRCHEX=$BUILDDIR/$basename.merged.hex
+ cp $SRCHEX $OUTDIR/
fi
if (echo $1 | grep -q "rak4631"); then
- echo "Copying hex file"
- cp .pio/build/$1/firmware.hex $OUTDIR/$basename.hex
-fi
\ No newline at end of file
+ echo "Copying .hex file"
+ cp $SRCHEX $OUTDIR/
+fi
+
+echo "Copying manifest"
+cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-rp2xx0.sh b/bin/build-rp2xx0.sh
index cb4865914..3ef1c1e34 100755
--- a/bin/build-rp2xx0.sh
+++ b/bin/build-rp2xx0.sh
@@ -5,7 +5,8 @@ set -e
VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short`
-OUTDIR=release/
+BUILDDIR=.pio/build/$1
+OUTDIR=release
rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true
@@ -14,20 +15,19 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
-rm -f .pio/build/$1/firmware.*
+rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find
export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
-SRCELF=.pio/build/$1/firmware.elf
-cp $SRCELF $OUTDIR/$basename.elf
+pio run --environment $1 -t mtjson # -v
+
+cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying uf2 file"
-SRCBIN=.pio/build/$1/firmware.uf2
-cp $SRCBIN $OUTDIR/$basename.uf2
+cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
-cp bin/device-install.* $OUTDIR
-cp bin/device-update.* $OUTDIR
+echo "Copying manifest"
+cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-stm32wl.sh b/bin/build-stm32wl.sh
index f62df4842..023f3603c 100755
--- a/bin/build-stm32wl.sh
+++ b/bin/build-stm32wl.sh
@@ -5,7 +5,8 @@ set -e
VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short)
-OUTDIR=release/
+BUILDDIR=.pio/build/$1
+OUTDIR=release
rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true
@@ -14,16 +15,19 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
-rm -f .pio/build/$1/firmware.*
+rm -f $BUILDDIR/firmware*
# The shell vars the build tool expects to find
export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
-SRCELF=.pio/build/$1/firmware.elf
-cp $SRCELF $OUTDIR/$basename.elf
+pio run --environment $1 -t mtjson # -v
-SRCBIN=.pio/build/$1/firmware.bin
-cp $SRCBIN $OUTDIR/$basename.bin
+cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
+
+echo "Copying STM32 bin file"
+cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
+
+echo "Copying manifest"
+cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml
index b4cc81792..adf804ba9 100644
--- a/bin/config-dist.yaml
+++ b/bin/config-dist.yaml
@@ -184,6 +184,8 @@ Input:
Logging:
LogLevel: info # debug, info, warn, error
# TraceFile: /var/log/meshtasticd.json
+# JSONFile: /packets.json # File location for JSON output of decoded packets
+# JSONFilter: position # filter for packets to save to JSON file
# AsciiLogs: true # default if not specified is !isatty() on stdout
Webserver:
diff --git a/bin/device-install.bat b/bin/device-install.bat
index 519073b08..c200a3201 100755
--- a/bin/device-install.bat
+++ b/bin/device-install.bat
@@ -5,22 +5,14 @@ TITLE Meshtastic device-install
SET "SCRIPT_NAME=%~nx0"
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="
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 t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv heltec-v4"
-SET "C3=esp32c3"
-@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
-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 heltec-v4"
+@REM Default offsets.
+@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
+SET "OTA_OFFSET=0x260000"
+SET "SPIFFS_OFFSET=0x300000"
GOTO getopts
:help
@@ -29,7 +21,7 @@ ECHO.
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset]
ECHO.
ECHO Options:
-ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
+ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required)
ECHO The file must be located in this current directory.
ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
ECHO If not set, ESPTOOL iterates all ports (Dangerous).
@@ -40,12 +32,12 @@ ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice.
ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
-ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
-ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11
+ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11
+ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11
GOTO eof
:version
-ECHO %SCRIPT_NAME% [Version 2.6.2]
+ECHO %SCRIPT_NAME% [Version 2.7.0]
ECHO Meshtastic
GOTO eof
@@ -78,8 +70,8 @@ IF "__!FILENAME!__"=="____" (
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
GOTO help
)
- IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" (
- CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file."
+ IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
+ CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file."
GOTO help
)
@REM Remove ".\" or "./" file prefix if present.
@@ -93,12 +85,26 @@ IF NOT EXIST !FILENAME! (
GOTO eof
)
-IF NOT "!FILENAME:update=!"=="!FILENAME!" (
- CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
- CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!."
- GOTO eof
+CALL :LOG_MESSAGE DEBUG "Checking for metadata..."
+@REM Derive metadata filename from firmware filename.
+SET "METAFILE=!FILENAME:.factory.bin=!.mt.json"
+IF EXIST !METAFILE! (
+ @REM Print parsed json with powershell
+ CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!"
+ powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()"
+
+ @REM Save metadata values to variables for later use.
+ FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
+ "(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A"
+ FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
+ "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"`
+ ) DO SET "OTA_OFFSET=%%A"
+ FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
+ "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"`
+ ) DO SET "SPIFFS_OFFSET=%%A"
) ELSE (
- CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
+ CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!"
+ GOTO eof
)
:skip-filename
@@ -108,7 +114,7 @@ IF NOT "__%PYTHON%__"=="____" (
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.
@@ -146,100 +152,26 @@ IF %BPS_RESET% EQU 1 (
GOTO eof
)
-@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
-@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
-IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
- CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
- SET "TFT_BUILD=1"
+@REM Extract PROGNAME from %FILENAME% for later use.
+SET "PROGNAME=!FILENAME:.factory.bin=!"
+CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!"
+
+IF "__!MCU!__" == "__esp32s3__" (
+ @REM We are working with ESP32-S3
+ SET "OTA_FILENAME=bleota-s3.bin"
+) ELSE IF "__!MCU!__" == "__esp32c3__" (
+ @REM We are working with ESP32-C3
+ SET "OTA_FILENAME=bleota-c3.bin"
) ELSE (
- CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
+ @REM Everything else
+ SET "OTA_FILENAME=bleota.bin"
)
-
-FOR %%a IN (%BIGDB_8MB%) DO (
- IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
- @REM We are working with any of %BIGDB_8MB%.
- SET "BIGDB8=1"
- GOTO end_loop_bigdb_8mb
- )
-)
-: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%.
- SET "BIGDB16=1"
- GOTO end_loop_bigdb_16mb
- )
-)
-: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.
-SET "BASENAME=!FILENAME:firmware-=!"
-CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!"
-
-@REM Account for S3 and C3 board's different OTA partition.
-FOR %%a IN (%S3%) DO (
- IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
- @REM We are working with any of %S3%.
- SET "OTA_FILENAME=bleota-s3.bin"
- GOTO :end_loop_s3
- )
-)
-
-FOR %%a IN (%C3%) DO (
- IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
- @REM We are working with any of %C3%.
- SET "OTA_FILENAME=bleota-c3.bin"
- GOTO :end_loop_c3
- )
-)
-
-@REM Everything else
-SET "OTA_FILENAME=bleota.bin"
-:end_loop_s3
-:end_loop_c3
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix.
-SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
+SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
-@REM Default offsets.
-@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
-SET "OTA_OFFSET=0x260000"
-SET "SPIFFS_OFFSET=0x300000"
-
-@REM Offsets for BigDB 8mb.
-IF %BIGDB8% EQU 1 (
- SET "OTA_OFFSET=0x340000"
- 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"
- SET "SPIFFS_OFFSET=0xc90000"
-)
-
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"
diff --git a/bin/device-install.sh b/bin/device-install.sh
index 69e4794ba..1778a952d 100755
--- a/bin/device-install.sh
+++ b/bin/device-install.sh
@@ -2,69 +2,15 @@
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false
-TFT_BUILD=false
MCU=""
# Constants
RESET_BAUD=1200
FIRMWARE_OFFSET=0x00
-
-# Variant groups
-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"
-)
-MUIDB_8MB=(
- "picomputer-s3"
- "unphone"
- "seeed-sensecap-indicator"
-)
-BIGDB_16MB=(
- "dreamcatcher"
- "elecrow-adv"
- "ESP32-S3-Pico"
- "heltec-v4"
- "m5stack-cores3"
- "mesh-tab"
- "station-g2"
- "t-deck"
- "t-energy-s3"
- "t-eth-elite"
- "t-watch-s3"
- "tlora-pager"
-)
-S3_VARIANTS=(
- "s3"
- "-v3"
- "-v4"
- "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"
-)
+# Default littlefs* offset.
+OFFSET=0x300000
+# Default OTA Offset
+OTA_OFFSET=0x260000
# Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then
@@ -78,6 +24,14 @@ else
exit 1
fi
+# Check for jq
+if ! command -v jq >/dev/null 2>&1; then
+ echo "Error: jq not found" >&2
+ echo "Install jq with your package manager." >&2
+ echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2
+ exit 1
+fi
+
set -e
# Usage info
@@ -89,7 +43,7 @@ Flash image file to device, but first erasing and writing system information.
-h Display this help and exit.
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
- -f FILENAME The firmware .bin file to flash. Custom to your device type and region.
+ -f FILENAME The firmware *.factory.bin file to flash. Custom to your device type and region.
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF
@@ -138,69 +92,43 @@ fi
shift
}
-if [[ "$FILENAME" != firmware-* ]]; then
- echo "Filename must be a firmware-* file."
+if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then
+ echo "Filename must be a firmware-*.factory.bin file."
exit 1
fi
-# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
-if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then
- TFT_BUILD=true
-fi
+# Extract PROGNAME from %FILENAME% for later use.
+PROGNAME="${FILENAME/.factory.bin/}"
+# Derive metadata filename from %PROGNAME%.
+METAFILE="${PROGNAME}.mt.json"
-# Extract BASENAME from %FILENAME% for later use.
-BASENAME="${FILENAME/firmware-/}"
-
-if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
- # Default littlefs* offset.
- OFFSET=0x300000
-
- # Default OTA Offset
- OTA_OFFSET=0x260000
-
- # littlefs* offset for BigDB 8mb and OTA OFFSET.
- for variant in "${BIGDB_8MB[@]}"; do
- if [ -z "${FILENAME##*"$variant"*}" ]; then
- OFFSET=0x670000
- OTA_OFFSET=0x340000
- 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
- OFFSET=0xc90000
- OTA_OFFSET=0x650000
- fi
- done
-
- # Account for S3 board's different OTA partition
- # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable
- for variant in "${S3_VARIANTS[@]}"; do
- if [ -z "${FILENAME##*"$variant"*}" ]; then
- MCU="esp32s3"
- fi
- done
-
- if [ "$MCU" != "esp32s3" ]; then
- if [ -n "${FILENAME##*"esp32c3"*}" ]; then
- OTAFILE=bleota.bin
- else
- OTAFILE=bleota-c3.bin
+if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then
+ # Display metadata if it exists
+ if [[ -f "$METAFILE" ]]; then
+ echo "Firmware metadata: ${METAFILE}"
+ jq . "$METAFILE"
+ # Extract relevant fields from metadata
+ if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then
+ OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE")
+ SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE")
fi
+ MCU=$(jq -r '.mcu' "$METAFILE")
else
+ echo "ERROR: No metadata file found at ${METAFILE}"
+ exit 1
+ fi
+
+ # Determine OTA filename based on MCU type
+ if [ "$MCU" == "esp32s3" ]; then
OTAFILE=bleota-s3.bin
+ elif [ "$MCU" == "esp32c3" ]; then
+ OTAFILE=bleota-c3.bin
+ else
+ OTAFILE=bleota.bin
fi
# Set SPIFFS filename with "littlefs-" prefix.
- SPIFFSFILE=littlefs-${BASENAME}
+ SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin"
if [[ ! -f "$FILENAME" ]]; then
echo "Error: file ${FILENAME} wasn't found. Terminating."
diff --git a/bin/device-update.bat b/bin/device-update.bat
index a263da992..a9f7a9e1e 100755
--- a/bin/device-update.bat
+++ b/bin/device-update.bat
@@ -30,11 +30,11 @@ ECHO --change-mode Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice.
ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode
-ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
+ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
GOTO eof
:version
-ECHO %SCRIPT_NAME% [Version 2.6.2]
+ECHO %SCRIPT_NAME% [Version 2.7.0]
ECHO Meshtastic
GOTO eof
@@ -78,12 +78,12 @@ IF NOT EXIST !FILENAME! (
GOTO eof
)
-IF "!FILENAME:update=!"=="!FILENAME!" (
- CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
+IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" (
+ CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!"
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!."
GOTO eof
) ELSE (
- CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
+ CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!"
)
:skip-filename
diff --git a/bin/device-update.sh b/bin/device-update.sh
index f64280a5b..1c3d6be70 100755
--- a/bin/device-update.sh
+++ b/bin/device-update.sh
@@ -29,7 +29,7 @@ Flash image file to device, leave existing system intact."
-h Display this help and exit
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
- -f FILENAME The *update.bin file to flash. Custom to your device type.
+ -f FILENAME The *.bin file to flash. Custom to your device type.
--change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF
@@ -78,7 +78,7 @@ fi
shift
}
-if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
+if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then
echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
else
diff --git a/bin/exception_decoder.py b/bin/exception_decoder.py
index ec94ce20e..ffe6d3f24 100755
--- a/bin/exception_decoder.py
+++ b/bin/exception_decoder.py
@@ -75,7 +75,7 @@ TOOLS = {
}
BACKTRACE_REGEX = re.compile(
- r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b"
+ r"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-F]{8}\b"
)
EXCEPTION_REGEX = re.compile("^Exception \\((?P[0-9]*)\\):$")
COUNTER_REGEX = re.compile(
@@ -89,7 +89,7 @@ POINTER_REGEX = re.compile(
STACK_BEGIN = ">>>stack>>>"
STACK_END = "<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$"
+ r"^(?P[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$"
)
StackLine = namedtuple("StackLine", ["offset", "content"])
@@ -223,7 +223,7 @@ class AddressResolver(object):
if match is None:
if last is not None and line.startswith("(inlined by)"):
line = line[12:].strip()
- self._address_map[last] += "\n \-> inlined by: " + line
+ self._address_map[last] += "\n \\-> inlined by: " + line
continue
if match.group("result") == "?? ??:0":
diff --git a/bin/native-gdbserver.sh b/bin/native-gdbserver.sh
index f779d6670..a45a2dc26 100755
--- a/bin/native-gdbserver.sh
+++ b/bin/native-gdbserver.sh
@@ -2,4 +2,4 @@
set -e
pio run --environment native
-gdbserver --once localhost:2345 .pio/build/native/program "$@"
+gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@"
diff --git a/bin/native-run.sh b/bin/native-run.sh
index 6566fc591..a8309c2d3 100755
--- a/bin/native-run.sh
+++ b/bin/native-run.sh
@@ -2,4 +2,4 @@
set -e
pio run --environment native
-.pio/build/native/program "$@"
+.pio/build/native/meshtasticd "$@"
diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml
index 243edca0c..140ac3e2a 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.17
+
https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16
diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py
index 4a1887d9d..b6560f35b 100644
--- a/bin/platformio-custom.py
+++ b/bin/platformio-custom.py
@@ -2,98 +2,82 @@
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import sys
-from os.path import join
+from os.path import join, basename, isfile
import subprocess
import json
import re
-import time
from datetime import datetime
from readprops import readProps
Import("env")
platform = env.PioPlatform()
+progname = env.get("PROGNAME")
+lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
-
-def esp32_create_combined_bin(source, target, env):
- # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3
- # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py
- print("Generating combined binary for serial flashing")
-
- app_offset = 0x10000
-
- new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
- sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
- firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
- chip = env.get("BOARD_MCU")
- flash_size = env.BoardConfig().get("upload.flash_size")
- flash_freq = env.BoardConfig().get("build.f_flash", "40m")
- flash_freq = flash_freq.replace("000000L", "m")
- flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
- memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
- if flash_mode == "qio" or flash_mode == "qout":
- flash_mode = "dio"
- if memory_type == "opi_opi" or memory_type == "opi_qspi":
- flash_mode = "dout"
- cmd = [
- "--chip",
- chip,
- "merge_bin",
- "-o",
- new_file_name,
- "--flash_mode",
- flash_mode,
- "--flash_freq",
- flash_freq,
- "--flash_size",
- flash_size,
+def manifest_gather(source, target, env):
+ out = []
+ board_platform = env.BoardConfig().get("platform")
+ needs_ota_suffix = board_platform == "nordicnrf52"
+ check_paths = [
+ progname,
+ f"{progname}.elf",
+ f"{progname}.bin",
+ f"{progname}.factory.bin",
+ f"{progname}.hex",
+ f"{progname}.merged.hex",
+ f"{progname}.uf2",
+ f"{progname}.factory.uf2",
+ f"{progname}.zip",
+ lfsbin
]
+ for p in check_paths:
+ f = env.File(env.subst(f"$BUILD_DIR/{p}"))
+ if f.exists():
+ manifest_name = p
+ if needs_ota_suffix and p == f"{progname}.zip":
+ manifest_name = f"{progname}-ota.zip"
+ d = {
+ "name": manifest_name,
+ "md5": f.get_content_hash(), # Returns MD5 hash
+ "bytes": f.get_size() # Returns file size in bytes
+ }
+ out.append(d)
+ print(d)
+ manifest_write(out, env)
- print(" Offset | File")
- for section in sections:
- sect_adr, sect_file = section.split(" ", 1)
- print(f" - {sect_adr} | {sect_file}")
- cmd += [sect_adr, sect_file]
+def manifest_write(files, env):
+ manifest = {
+ "version": verObj["long"],
+ "build_epoch": build_epoch,
+ "board": env.get("PIOENV"),
+ "mcu": env.get("BOARD_MCU"),
+ "repo": repo_owner,
+ "files": files,
+ "part": None,
+ "has_mui": False,
+ "has_inkhud": False,
+ }
+ # Get partition table (generated in esp32_pre.py) if it exists
+ if env.get("custom_mtjson_part"):
+ # custom_mtjson_part is a JSON string, convert it back to a dict
+ pj = json.loads(env.get("custom_mtjson_part"))
+ manifest["part"] = pj
+ # Enable has_mui for TFT builds
+ if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
+ manifest["has_mui"] = True
+ if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
+ manifest["has_inkhud"] = True
- print(f" - {hex(app_offset)} | {firmware_name}")
- cmd += [hex(app_offset), firmware_name]
-
- print("Using esptool.py arguments: %s" % " ".join(cmd))
-
- esptool.main(cmd)
-
-
-if platform.name == "espressif32":
- sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
- import esptool
-
- env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
-
- esp32_kind = env.GetProjectOption("custom_esp32_kind")
- if esp32_kind == "esp32":
- # Free up some IRAM by removing auxiliary SPI flash chip drivers.
- # Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
- env.Append(
- LINKFLAGS=[
- "-Wl,--wrap=esp_flash_chip_gd",
- "-Wl,--wrap=esp_flash_chip_issi",
- "-Wl,--wrap=esp_flash_chip_winbond",
- ]
- )
- else:
- # For newer ESP32 targets, using newlib nano works better.
- env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
-
-if platform.name == "nordicnrf52":
- env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
- env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"",
- "Generating UF2 file"))
+ # Write the manifest to the build directory
+ with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f:
+ json.dump(manifest, f, indent=2)
Import("projenv")
prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
-print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
+print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}")
# get repository owner if git is installed
try:
@@ -139,10 +123,10 @@ flags = [
"-DBUILD_EPOCH=" + str(build_epoch),
] + pref_flags
-print ("Using flags:")
+print("Using flags:")
for flag in flags:
print(flag)
-
+
projenv.Append(
CCFLAGS=flags,
)
@@ -180,4 +164,22 @@ def load_boot_logo(source, target, env):
# Load the boot logo on TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
- env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
+ env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
+
+mtjson_deps = ["buildprog"]
+if platform.name == "espressif32":
+ # Build littlefs image as part of mtjson target
+ # Equivalent to `pio run -t buildfs`
+ target_lfs = env.DataToBin(
+ join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
+ )
+ mtjson_deps.append(target_lfs)
+
+env.AddCustomTarget(
+ name="mtjson",
+ dependencies=mtjson_deps,
+ actions=[manifest_gather],
+ title="Meshtastic Manifest",
+ description="Generating Meshtastic manifest JSON + Checksums",
+ always_build=False,
+)
diff --git a/bin/platformio-pre.py b/bin/platformio-pre.py
new file mode 100644
index 000000000..16278b813
--- /dev/null
+++ b/bin/platformio-pre.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# trunk-ignore-all(ruff/F821)
+# trunk-ignore-all(flake8/F821): For SConstruct imports
+Import("env")
+platform = env.PioPlatform()
+
+if platform.name == "native":
+ env.Replace(PROGNAME="meshtasticd")
+else:
+ from readprops import readProps
+ prefsLoc = env["PROJECT_DIR"] + "/version.properties"
+ verObj = readProps(prefsLoc)
+ env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
+ env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
+
+# Print the new program name for verification
+print(f"PROGNAME: {env.get('PROGNAME')}")
+if platform.name == "espressif32":
+ print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")
diff --git a/bin/test-simulator.sh b/bin/test-simulator.sh
index 3c5f8f811..92ed21a7a 100755
--- a/bin/test-simulator.sh
+++ b/bin/test-simulator.sh
@@ -3,7 +3,7 @@
set -e
echo "Starting simulator"
-.pio/build/native/program &
+.pio/build/native/meshtasticd -s &
sleep 20 # 5 seconds was not enough
echo "Simulator started, launching python test..."
diff --git a/boards/hackaday-communicator.json b/boards/hackaday-communicator.json
new file mode 100644
index 000000000..6e6c1ad2d
--- /dev/null
+++ b/boards/hackaday-communicator.json
@@ -0,0 +1,41 @@
+{
+ "build": {
+ "arduino": {
+ "ldscript": "esp32s3_out.ld",
+ "memory_type": "qio_opi"
+ },
+ "core": "esp32",
+ "extra_flags": [
+ "-DBOARD_HAS_PSRAM",
+ "-DARDUINO_USB_CDC_ON_BOOT=1",
+ "-DARDUINO_USB_MODE=0",
+ "-DARDUINO_RUNNING_CORE=1",
+ "-DARDUINO_EVENT_RUNNING_CORE=1"
+ ],
+ "f_cpu": "240000000L",
+ "f_flash": "80000000L",
+ "flash_mode": "qio",
+ "hwids": [["0x303A", "0x1001"]],
+ "mcu": "esp32s3",
+ "variant": "hackaday-communicator"
+ },
+ "connectivity": ["wifi", "bluetooth", "lora"],
+ "debug": {
+ "default_tool": "esp-builtin",
+ "onboard_tools": ["esp-builtin"],
+ "openocd_target": "esp32s3.cfg"
+ },
+ "frameworks": ["arduino", "espidf"],
+ "name": "hackaday-communicator (16 MB FLASH, 8 MB PSRAM)",
+ "upload": {
+ "flash_size": "16MB",
+ "maximum_ram_size": 327680,
+ "maximum_size": 16777216,
+ "use_1200bps_touch": true,
+ "wait_for_upload_port": true,
+ "require_upload_port": true,
+ "speed": 1500000
+ },
+ "url": "hackaday.com",
+ "vendor": "hackaday"
+}
diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json
index 8eac3a9b2..9827be83f 100644
--- a/boards/heltec_v4.json
+++ b/boards/heltec_v4.json
@@ -9,7 +9,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
- "-DARDUINO_USB_MODE=0",
+ "-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
diff --git a/debian/changelog b/debian/changelog
index 5a0f543eb..b9212c1be 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+meshtasticd (2.7.17.0) unstable; urgency=medium
+
+ * Version 2.7.17
+
+ -- GitHub Actions Fri, 28 Nov 2025 15:11:34 +0000
+
meshtasticd (2.7.16.0) unstable; urgency=medium
* Version 2.7.16
diff --git a/debian/rules b/debian/rules
index 0b5d1ac57..ebb572153 100755
--- a/debian/rules
+++ b/debian/rules
@@ -28,5 +28,4 @@ override_dh_auto_build:
# Build with platformio
$(PIO_ENV) platformio run -e native-tft
# Move the binary and default config to the correct name
- mv .pio/build/native-tft/program .pio/build/native-tft/meshtasticd
cp bin/config-dist.yaml bin/config.yaml
diff --git a/extra_scripts/disable_adafruit_usb.py b/extra_scripts/disable_adafruit_usb.py
index 596242184..3b901e2db 100644
--- a/extra_scripts/disable_adafruit_usb.py
+++ b/extra_scripts/disable_adafruit_usb.py
@@ -1,10 +1,9 @@
+#!/usr/bin/env python3
# trunk-ignore-all(flake8/F821)
# trunk-ignore-all(ruff/F821)
Import("env")
-# NOTE: This is not currently used, but can serve as an example on how to write extra_scripts
-
# print("Current CLI targets", COMMAND_LINE_TARGETS)
# print("Current Build targets", BUILD_TARGETS)
# print("CPP defs", env.get("CPPDEFINES"))
diff --git a/extra_scripts/esp32_extra.py b/extra_scripts/esp32_extra.py
new file mode 100755
index 000000000..f7698561a
--- /dev/null
+++ b/extra_scripts/esp32_extra.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# trunk-ignore-all(ruff/F821)
+# trunk-ignore-all(flake8/F821): For SConstruct imports
+# trunk-ignore-all(ruff/E402): Hacky esptool import
+# trunk-ignore-all(flake8/E402): Hacky esptool import
+import sys
+from os.path import join
+
+Import("env")
+platform = env.PioPlatform()
+
+sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
+# IntelHex workaround, remove after fixed upstream
+# https://github.com/platformio/platform-espressif32/issues/1632
+try:
+ import intelhex
+except ImportError:
+ env.Execute("$PYTHONEXE -m pip install intelhex")
+import esptool
+
+
+def esp32_create_combined_bin(source, target, env):
+ # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3
+ # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py
+ print("Generating combined binary for serial flashing")
+
+ app_offset = 0x10000
+
+ new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
+ sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
+ firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
+ chip = env.get("BOARD_MCU")
+ board = env.BoardConfig()
+ flash_size = board.get("upload.flash_size")
+ flash_freq = board.get("build.f_flash", "40m")
+ flash_freq = flash_freq.replace("000000L", "m")
+ flash_mode = board.get("build.flash_mode", "dio")
+ memory_type = board.get("build.arduino.memory_type", "qio_qspi")
+ if flash_mode == "qio" or flash_mode == "qout":
+ flash_mode = "dio"
+ if memory_type == "opi_opi" or memory_type == "opi_qspi":
+ flash_mode = "dout"
+ cmd = [
+ "--chip",
+ chip,
+ "merge_bin",
+ "-o",
+ new_file_name,
+ "--flash_mode",
+ flash_mode,
+ "--flash_freq",
+ flash_freq,
+ "--flash_size",
+ flash_size,
+ ]
+
+ print(" Offset | File")
+ for section in sections:
+ sect_adr, sect_file = section.split(" ", 1)
+ print(f" - {sect_adr} | {sect_file}")
+ cmd += [sect_adr, sect_file]
+
+ print(f" - {hex(app_offset)} | {firmware_name}")
+ cmd += [hex(app_offset), firmware_name]
+
+ print("Using esptool.py arguments: %s" % " ".join(cmd))
+
+ esptool.main(cmd)
+
+
+env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
+
+esp32_kind = env.GetProjectOption("custom_esp32_kind")
+if esp32_kind == "esp32":
+ # Free up some IRAM by removing auxiliary SPI flash chip drivers.
+ # Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
+ env.Append(
+ LINKFLAGS=[
+ "-Wl,--wrap=esp_flash_chip_gd",
+ "-Wl,--wrap=esp_flash_chip_issi",
+ "-Wl,--wrap=esp_flash_chip_winbond",
+ ]
+ )
+else:
+ # For newer ESP32 targets, using newlib nano works better.
+ env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
diff --git a/extra_scripts/esp32_pre.py b/extra_scripts/esp32_pre.py
new file mode 100755
index 000000000..8e21770e9
--- /dev/null
+++ b/extra_scripts/esp32_pre.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# trunk-ignore-all(ruff/F821)
+# trunk-ignore-all(flake8/F821): For SConstruct imports
+import json
+import sys
+from os.path import isfile
+
+Import("env")
+
+
+# From https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py
+def _parse_size(value):
+ if isinstance(value, int):
+ return value
+ elif value.isdigit():
+ return int(value)
+ elif value.startswith("0x"):
+ return int(value, 16)
+ elif value[-1].upper() in ("K", "M"):
+ base = 1024 if value[-1].upper() == "K" else 1024 * 1024
+ return int(value[:-1]) * base
+ return value
+
+
+def _parse_partitions(env):
+ partitions_csv = env.subst("$PARTITIONS_TABLE_CSV")
+ if not isfile(partitions_csv):
+ sys.stderr.write(
+ "Could not find the file %s with partitions " "table.\n" % partitions_csv
+ )
+ env.Exit(1)
+ return
+
+ result = []
+ # The first offset is 0x9000 because partition table is flashed to 0x8000 and
+ # occupies an entire flash sector, which size is 0x1000
+ next_offset = 0x9000
+ with open(partitions_csv) as fp:
+ for line in fp.readlines():
+ line = line.strip()
+ if not line or line.startswith("#"):
+ continue
+ tokens = [t.strip() for t in line.split(",")]
+ if len(tokens) < 5:
+ continue
+
+ bound = 0x10000 if tokens[1] in ("0", "app") else 4
+ calculated_offset = (next_offset + bound - 1) & ~(bound - 1)
+ partition = {
+ "name": tokens[0],
+ "type": tokens[1],
+ "subtype": tokens[2],
+ "offset": tokens[3] or calculated_offset,
+ "size": tokens[4],
+ "flags": tokens[5] if len(tokens) > 5 else None,
+ }
+ result.append(partition)
+ next_offset = _parse_size(partition["offset"]) + _parse_size(
+ partition["size"]
+ )
+
+ return result
+
+
+def mtjson_esp32_part(target, source, env):
+ part = _parse_partitions(env)
+ pj = json.dumps(part)
+ # print(f"JSON_PARTITIONS: {pj}")
+ # Dump json string to 'custom_mtjson_part' variable to use later when writing the manifest
+ env.Replace(custom_mtjson_part=pj)
+
+
+env.AddPreAction("mtjson", mtjson_esp32_part)
diff --git a/extra_scripts/nrf52_extra.py b/extra_scripts/nrf52_extra.py
new file mode 100755
index 000000000..8e95e42bf
--- /dev/null
+++ b/extra_scripts/nrf52_extra.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# trunk-ignore-all(ruff/F821)
+# trunk-ignore-all(flake8/F821): For SConstruct imports
+
+import sys
+from os.path import basename
+
+Import("env")
+
+
+# Custom HEX from ELF
+# Convert hex to uf2 for nrf52
+def nrf52_hex_to_uf2(source, target, env):
+ hex_path = target[0].get_abspath()
+ # When using merged hex, drop 'merged' from uf2 filename
+ uf2_path = hex_path.replace(".merged.", ".")
+ uf2_path = uf2_path.replace(".hex", ".uf2")
+ env.Execute(
+ env.VerboseAction(
+ f'"{sys.executable}" ./bin/uf2conv.py "{hex_path}" -c -f 0xADA52840 -o "{uf2_path}"',
+ f"Generating UF2 file from {basename(hex_path)}",
+ )
+ )
+
+
+def nrf52_mergehex(source, target, env):
+ hex_path = target[0].get_abspath()
+ merged_hex_path = hex_path.replace(".hex", ".merged.hex")
+ merge_with = None
+ if "wio-sdk-wm1110" == str(env.get("PIOENV")):
+ merge_with = env.subst("$PROJECT_DIR/bin/s140_nrf52_7.3.0_softdevice.hex")
+ else:
+ print("merge_with not defined for this target")
+
+ if merge_with is not None:
+ env.Execute(
+ env.VerboseAction(
+ f'"$PROJECT_DIR/bin/mergehex" -m "{hex_path}" "{merge_with}" -o "{merged_hex_path}"',
+ "Merging HEX with SoftDevice",
+ )
+ )
+ print(f'Merged file saved at "{basename(merged_hex_path)}"')
+ nrf52_hex_to_uf2([hex_path, merge_with], [env.File(merged_hex_path)], env)
+
+
+# if WM1110 target, merge hex with softdevice 7.3.0
+if "wio-sdk-wm1110" == env.get("PIOENV"):
+ env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_mergehex)
+else:
+ env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_hex_to_uf2)
diff --git a/extra_scripts/extra_stm32.py b/extra_scripts/stm32_extra.py
similarity index 95%
rename from extra_scripts/extra_stm32.py
rename to extra_scripts/stm32_extra.py
index f3bd8c514..afceb7d81 100755
--- a/extra_scripts/extra_stm32.py
+++ b/extra_scripts/stm32_extra.py
@@ -1,7 +1,9 @@
+#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
Import("env")
+
# Custom HEX from ELF
env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.elf",
diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg
index e2da172c3..3456001f0 100644
--- a/meshtasticd.spec.rpkg
+++ b/meshtasticd.spec.rpkg
@@ -76,7 +76,7 @@ platformio run -e native-tft
%install
# Install meshtasticd binary
mkdir -p %{buildroot}%{_bindir}
-install -m 0755 .pio/build/native-tft/program %{buildroot}%{_bindir}/meshtasticd
+install -m 0755 .pio/build/native-tft/meshtasticd %{buildroot}%{_bindir}/meshtasticd
# Install portduino VFS dir
install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd
diff --git a/platformio.ini b/platformio.ini
index f560bd8f5..3dadee33b 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -5,7 +5,7 @@
default_envs = tbeam
extra_configs =
- arch/*/*.ini
+ variants/*/*.ini
variants/*/*/platformio.ini
variants/*/diy/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini
@@ -14,7 +14,9 @@ description = Meshtastic
[env]
test_build_src = true
-extra_scripts = bin/platformio-custom.py
+extra_scripts =
+ pre:bin/platformio-pre.py
+ bin/platformio-custom.py
; note: we add src to our include search path so that lmic_project_config can override
; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile
; of code is a heap corruption bug!
@@ -62,7 +64,7 @@ monitor_speed = 115200
monitor_filters = direct
lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
- https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2887bf4a19f64d92c984dcc8fd5ca7429e425e4a.zip
+ https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -121,7 +123,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/4fb5f24787caa841b58dbf623a52c4c5861d6722.zip
+ https://github.com/meshtastic/device-ui/archive/d234bd98c7d293d5c17fbf6dfe6797238d39805e.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -182,8 +184,8 @@ lib_deps =
dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2
- # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/BH1750_WE@^1.1.10
- wollewald/BH1750_WE@^1.1.10
+ # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE
+ wollewald/BH1750_WE@1.1.10
; (not included in native / portduino)
[environmental_extra]
@@ -205,7 +207,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
- ClosedCube OPT3001@1.1.2
+ closedcube/ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
diff --git a/protobufs b/protobufs
index 4095e5989..9beb80f1d 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 4095e598902b4cd893dbcb62842514704d0f64e0
+Subproject commit 9beb80f1d302f70d05f9c4bc9dd543b8f7bc8796
diff --git a/src/AudioThread.h b/src/AudioThread.h
index df4892b6e..23552c421 100644
--- a/src/AudioThread.h
+++ b/src/AudioThread.h
@@ -50,8 +50,11 @@ class AudioThread : public concurrency::OSThread
delete i2sRtttl;
i2sRtttl = nullptr;
}
- delete rtttlFile;
- rtttlFile = nullptr;
+
+ if (rtttlFile != nullptr) {
+ delete rtttlFile;
+ rtttlFile = nullptr;
+ }
setCPUFast(false);
#ifdef T_LORA_PAGER
@@ -99,9 +102,9 @@ class AudioThread : public concurrency::OSThread
};
AudioGeneratorRTTTL *i2sRtttl = nullptr;
- AudioOutputI2S *audioOut;
+ AudioOutputI2S *audioOut = nullptr;
- AudioFileSourcePROGMEM *rtttlFile;
+ AudioFileSourcePROGMEM *rtttlFile = nullptr;
};
#endif
diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp
index 246cf0022..d88f9fc9f 100644
--- a/src/DisplayFormatters.cpp
+++ b/src/DisplayFormatters.cpp
@@ -31,6 +31,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return useShortName ? "LongF" : "LongFast";
break;
+ case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
+ return useShortName ? "LongT" : "LongTurbo";
+ break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
return useShortName ? "LongM" : "LongMod";
break;
diff --git a/src/Power.cpp b/src/Power.cpp
index a2c559d91..7bb8896ce 100644
--- a/src/Power.cpp
+++ b/src/Power.cpp
@@ -1453,7 +1453,7 @@ class LipoCharger : public HasBatteryLevel
/**
* return true if there is an external power source detected
*/
- virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; }
+ virtual bool isVbusIn() override { return PPM->isVbusIn(); }
/**
* return true if the battery is currently charging
diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp
index 322b877ff..9f8097b84 100644
--- a/src/PowerFSM.cpp
+++ b/src/PowerFSM.cpp
@@ -57,21 +57,21 @@ static bool isPowered()
static void sdsEnter()
{
- LOG_DEBUG("State: SDS");
+ LOG_POWERFSM("State: SDS");
// FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw
doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false);
}
static void lowBattSDSEnter()
{
- LOG_DEBUG("State: Lower batt SDS");
+ LOG_POWERFSM("State: Lower batt SDS");
doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true);
}
extern Power *power;
static void shutdownEnter()
{
- LOG_DEBUG("State: SHUTDOWN");
+ LOG_POWERFSM("State: SHUTDOWN");
shutdownAtMsec = millis();
}
@@ -81,7 +81,7 @@ static uint32_t secsSlept;
static void lsEnter()
{
- LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs);
+ LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs);
if (screen)
screen->setOn(false);
secsSlept = 0; // How long have we been sleeping this time
@@ -155,12 +155,12 @@ static void lsIdle()
static void lsExit()
{
- LOG_INFO("Exit state: LS");
+ LOG_POWERFSM("State: lsExit");
}
static void nbEnter()
{
- LOG_DEBUG("State: NB");
+ LOG_POWERFSM("State: nbEnter");
if (screen)
screen->setOn(false);
#ifdef ARCH_ESP32
@@ -173,6 +173,7 @@ static void nbEnter()
static void darkEnter()
{
+ LOG_POWERFSM("State: darkEnter");
setBluetoothEnable(true);
if (screen)
screen->setOn(false);
@@ -180,7 +181,7 @@ static void darkEnter()
static void serialEnter()
{
- LOG_DEBUG("State: SERIAL");
+ LOG_POWERFSM("State: serialEnter");
setBluetoothEnable(false);
if (screen) {
screen->setOn(true);
@@ -189,13 +190,14 @@ static void serialEnter()
static void serialExit()
{
+ LOG_POWERFSM("State: serialExit");
// Turn bluetooth back on when we leave serial stream API
setBluetoothEnable(true);
}
static void powerEnter()
{
- // LOG_DEBUG("State: POWER");
+ LOG_POWERFSM("State: powerEnter");
if (!isPowered()) {
// If we got here, we are in the wrong state - we should be in powered, let that state handle things
LOG_INFO("Loss of power in Powered");
@@ -210,6 +212,7 @@ static void powerEnter()
static void powerIdle()
{
+ // LOG_POWERFSM("State: powerIdle"); // very chatty
if (!isPowered()) {
// If we got here, we are in the wrong state
LOG_INFO("Loss of power in Powered");
@@ -219,14 +222,13 @@ static void powerIdle()
static void powerExit()
{
- if (screen)
- screen->setOn(true);
+ LOG_POWERFSM("State: powerExit");
setBluetoothEnable(true);
}
static void onEnter()
{
- LOG_DEBUG("State: ON");
+ LOG_POWERFSM("State: onEnter");
if (screen)
screen->setOn(true);
setBluetoothEnable(true);
@@ -234,6 +236,7 @@ static void onEnter()
static void onIdle()
{
+ LOG_POWERFSM("State: onIdle");
if (isPowered()) {
// If we got here, we are in the wrong state - we should be in powered, let that state handle things
powerFSM.trigger(EVENT_POWER_CONNECTED);
@@ -242,7 +245,7 @@ static void onIdle()
static void bootEnter()
{
- LOG_DEBUG("State: BOOT");
+ LOG_POWERFSM("State: bootEnter");
}
State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN");
@@ -319,11 +322,6 @@ void PowerFSM_setup()
// if any packet destined for phone arrives, turn on bluetooth at least
powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone");
- // Removed 2.7: we don't show the nodes individually for every node on the screen anymore
- // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
- // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
- // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
-
// Show the received text message
powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
@@ -372,7 +370,7 @@ void PowerFSM_setup()
// Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated
// through the modules
-#if HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI)
+#if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI)
bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR;
diff --git a/src/PowerFSM.h b/src/PowerFSM.h
index 6330a5fc6..182ac082a 100644
--- a/src/PowerFSM.h
+++ b/src/PowerFSM.h
@@ -2,6 +2,12 @@
#include "configuration.h"
+#ifdef PowerFSMDebug
+#define LOG_POWERFSM(...) LOG_DEBUG(__VA_ARGS__)
+#else
+#define LOG_POWERFSM(...)
+#endif
+
// See sw-design.md for documentation
#define EVENT_PRESS 1
diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp
index b0d162a44..aa8346585 100644
--- a/src/buzz/buzz.cpp
+++ b/src/buzz/buzz.cpp
@@ -16,6 +16,7 @@ struct ToneDuration {
};
// Some common frequencies.
+#define NOTE_SILENT 1
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
@@ -29,11 +30,16 @@ struct ToneDuration {
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_CS4 277
+#define NOTE_B4 494
+#define NOTE_F5 698
+#define NOTE_G6 1568
+#define NOTE_E7 2637
+const int DURATION_1_16 = 62; // 1/16 note
const int DURATION_1_8 = 125; // 1/8 note
const int DURATION_1_4 = 250; // 1/4 note
const int DURATION_1_2 = 500; // 1/2 note
-const int DURATION_3_4 = 750; // 1/4 note
+const int DURATION_3_4 = 750; // 3/4 note
const int DURATION_1_1 = 1000; // 1/1 note
void playTones(const ToneDuration *tone_durations, int size)
@@ -71,13 +77,24 @@ void playLongBeep()
void playGPSEnableBeep()
{
+#if defined(R1_NEO) || defined(MUZI_BASE)
+ ToneDuration melody[] = {
+ {NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}};
+#else
ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}};
+#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playGPSDisableBeep()
{
+#if defined(R1_NEO) || defined(MUZI_BASE)
+ ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
+ {NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
+ {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}};
+#else
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}};
+#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
diff --git a/src/configuration.h b/src/configuration.h
index d30280d8b..b4ab57053 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -36,6 +36,29 @@ along with this program. If not, see .
/* Offer chance for variant-specific defines */
#include "variant.h"
+// -----------------------------------------------------------------------------
+// Display feature overrides
+// -----------------------------------------------------------------------------
+
+// Allow build environments to opt-in explicitly to the E-Ink UI stack while
+// keeping headless targets slim by default. Existing variants that already
+// define USE_EINK continue to work without additional flags.
+#ifndef MESHTASTIC_USE_EINK_UI
+#ifdef USE_EINK
+#define MESHTASTIC_USE_EINK_UI 1
+#else
+#define MESHTASTIC_USE_EINK_UI 0
+#endif
+#endif
+
+#if MESHTASTIC_USE_EINK_UI
+#ifndef USE_EINK
+#define USE_EINK
+#endif
+#else
+#undef USE_EINK
+#endif
+
// -----------------------------------------------------------------------------
// Version
// -----------------------------------------------------------------------------
@@ -371,6 +394,9 @@ along with this program. If not, see .
#ifndef HAS_BLUETOOTH
#define HAS_BLUETOOTH 0
#endif
+#ifndef USE_TFTDISPLAY
+#define USE_TFTDISPLAY 0
+#endif
#ifndef HW_VENDOR
#error HW_VENDOR must be defined
diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp
index 0404ae5f8..a61a71dde 100644
--- a/src/gps/GPS.cpp
+++ b/src/gps/GPS.cpp
@@ -38,14 +38,16 @@ template std::size_t array_count(const T (&)[N])
return N;
}
-#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
-#if defined(GPS_SERIAL_PORT)
-HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
-#else
-HardwareSerial *GPS::_serial_gps = &Serial1;
+#ifndef GPS_SERIAL_PORT
+#define GPS_SERIAL_PORT Serial1
#endif
+
+#if defined(ARCH_NRF52)
+Uart *GPS::_serial_gps = &GPS_SERIAL_PORT;
+#elif defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
+HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
#elif defined(ARCH_RP2040)
-SerialUART *GPS::_serial_gps = &Serial1;
+SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
#else
HardwareSerial *GPS::_serial_gps = nullptr;
#endif
@@ -1525,10 +1527,7 @@ GPS *GPS::createGps()
int8_t _rx_gpio = config.position.rx_gpio;
int8_t _tx_gpio = config.position.tx_gpio;
int8_t _en_gpio = config.position.gps_en_gpio;
-#if HAS_GPS && !defined(ARCH_ESP32)
- _rx_gpio = 1; // We only specify GPS serial ports on ESP32. Otherwise, these are just flags.
- _tx_gpio = 1;
-#endif
+
#if defined(GPS_RX_PIN)
if (!_rx_gpio)
_rx_gpio = GPS_RX_PIN;
@@ -1602,16 +1601,28 @@ GPS *GPS::createGps()
_serial_gps->setRxBufferSize(SERIAL_BUFFER_SIZE); // the default is 256
#endif
-// ESP32 has a special set of parameters vs other arduino ports
-#if defined(ARCH_ESP32)
LOG_DEBUG("Use GPIO%d for GPS RX", new_gps->rx_gpio);
LOG_DEBUG("Use GPIO%d for GPS TX", new_gps->tx_gpio);
+
+// ESP32 has a special set of parameters vs other arduino ports
+#if defined(ARCH_ESP32)
_serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, new_gps->rx_gpio, new_gps->tx_gpio);
#elif defined(ARCH_RP2040)
+ _serial_gps->setPinout(new_gps->tx_gpio, new_gps->rx_gpio);
_serial_gps->setFIFOSize(256);
_serial_gps->begin(GPS_BAUDRATE);
-#else
+#elif defined(ARCH_NRF52)
+ _serial_gps->setPins(new_gps->rx_gpio, new_gps->tx_gpio);
_serial_gps->begin(GPS_BAUDRATE);
+#elif defined(ARCH_STM32WL)
+ _serial_gps->setTx(new_gps->tx_gpio);
+ _serial_gps->setRx(new_gps->rx_gpio);
+ _serial_gps->begin(GPS_BAUDRATE);
+#elif defined(ARCH_PORTDUINO)
+ // Portduino can't set the GPS pins directly.
+ _serial_gps->begin(GPS_BAUDRATE);
+#else
+#error Unsupported architecture!
#endif
}
return new_gps;
diff --git a/src/gps/GPS.h b/src/gps/GPS.h
index 8ba1ce0a6..59cee7113 100644
--- a/src/gps/GPS.h
+++ b/src/gps/GPS.h
@@ -194,6 +194,8 @@ class GPS : private concurrency::OSThread
/** If !NULL we will use this serial port to construct our GPS */
#if defined(ARCH_RP2040)
static SerialUART *_serial_gps;
+#elif defined(ARCH_NRF52)
+ static Uart *_serial_gps;
#else
static HardwareSerial *_serial_gps;
#endif
diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index 926690cd1..a1c74251d 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -70,7 +70,11 @@ using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
+#if USE_TFTDISPLAY
extern uint16_t TFT_MESH;
+#else
+uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
+#endif
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
#include "mesh/wifi/WiFiAPClient.h"
@@ -227,24 +231,9 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
- if (NotificationRenderer::virtualKeyboard) {
- delete NotificationRenderer::virtualKeyboard;
- NotificationRenderer::virtualKeyboard = nullptr;
- }
-
- NotificationRenderer::textInputCallback = nullptr;
-
- NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
- if (header) {
- NotificationRenderer::virtualKeyboard->setHeader(header);
- }
- if (initialText) {
- NotificationRenderer::virtualKeyboard->setInputText(initialText);
- }
-
- // Set up callback with safer cleanup mechanism
+ // Start OnScreenKeyboardModule session (non-touch variant)
+ OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
NotificationRenderer::textInputCallback = textCallback;
- NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
@@ -364,11 +353,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
#else
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
#endif
-#if defined(USE_ST7789)
- static_cast(dispdev)->setRGB(TFT_MESH);
-#elif defined(USE_ST7796)
- static_cast(dispdev)->setRGB(TFT_MESH);
-#endif
#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);
@@ -381,7 +365,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
LOG_INFO("SSD1306 init success");
}
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
- defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
+ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
@@ -413,6 +397,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
isAUTOOled = true;
#endif
+#if defined(USE_ST7789)
+ static_cast(dispdev)->setRGB(TFT_MESH);
+#elif defined(USE_ST7796)
+ static_cast(dispdev)->setRGB(TFT_MESH);
+#endif
+
ui = new OLEDDisplayUi(dispdev);
cmdQueue.setReader(this);
}
@@ -658,7 +648,7 @@ void Screen::setup()
#else
if (!config.display.flip_screen) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
- defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
+ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
static_cast(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789)
static_cast(dispdev)->flipScreenVertically();
@@ -1517,14 +1507,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Incoming message
devicestate.has_rx_text_message = true; // Needed to include the message frame
hasUnreadMessage = true; // Enables mail icon in the header
- setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
+ setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input)
// Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw
}
- // === Prepare banner content ===
+ // === Prepare banner/popup content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
@@ -1548,38 +1538,84 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
// 'mute' preferences set to any specific node or channel.
- if (isAlert) {
- if (longName && longName[0]) {
- snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
- } else {
- strcpy(banner, "Alert Received");
+ // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it
+ if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
+ // Wake and force redraw so popup is visible immediately
+ if (shouldWakeOnReceivedMessage()) {
+ setOn(true);
+ forceDisplay();
}
- screen->showSimpleBanner(banner, 3000);
- } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
- if (longName && longName[0]) {
-#if defined(M5STACK_UNITC6L)
- strcpy(banner, "New Message");
-#else
- snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
-#endif
+ // Build popup: title = message source name, content = message text (sanitized)
+ // Title
+ char titleBuf[64] = {0};
+ if (longName && longName[0]) {
+ // Sanitize sender name
+ std::string t = sanitizeString(longName);
+ strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
} else {
- strcpy(banner, "New Message");
+ strncpy(titleBuf, "Message", sizeof(titleBuf) - 1);
}
+
+ // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize
+ char content[256] = {0};
+ {
+ std::string raw;
+ raw.reserve(packet->decoded.payload.size);
+ for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
+ char c = msgRaw[i];
+ if (c == ASCII_BELL)
+ continue; // strip bell
+ raw.push_back(c);
+ }
+ std::string sanitized = sanitizeString(raw);
+ strncpy(content, sanitized.c_str(), sizeof(content) - 1);
+ }
+
+ NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
+
+// Maintain existing buzzer behavior on M5 if applicable
#if defined(M5STACK_UNITC6L)
- screen->setOn(true);
- screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) {
- // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
- // - packet contains an alert and alert bell buzzer is enabled
- // - packet is a non-broadcast that is addressed to this node
playLongBeep();
}
-#else
- screen->showSimpleBanner(banner, 3000);
#endif
+ } else {
+ // No keyboard active: use regular banner flow, respecting mute settings
+ if (isAlert) {
+ if (longName && longName[0]) {
+ snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
+ } else {
+ strcpy(banner, "Alert Received");
+ }
+ screen->showSimpleBanner(banner, 3000);
+ } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
+ if (longName && longName[0]) {
+#if defined(M5STACK_UNITC6L)
+ strcpy(banner, "New Message");
+#else
+ snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
+#endif
+ } else {
+ strcpy(banner, "New Message");
+ }
+#if defined(M5STACK_UNITC6L)
+ screen->setOn(true);
+ screen->showSimpleBanner(banner, 1500);
+ if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
+ (isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
+ (!isBroadcast(packet->to) && isToUs(packet))) {
+ // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
+ // - packet contains an alert and alert bell buzzer is enabled
+ // - packet is a non-broadcast that is addressed to this node
+ playLongBeep();
+ }
+#else
+ screen->showSimpleBanner(banner, 3000);
+#endif
+ }
}
}
}
@@ -1614,6 +1650,7 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
int Screen::handleInputEvent(const InputEvent *event)
{
+ LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar);
if (!screenOn)
return 0;
@@ -1661,6 +1698,12 @@ int Screen::handleInputEvent(const InputEvent *event)
showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showNextFrame();
+ } else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
+ // Long press up button for fast frame switching
+ showPrevFrame();
+ } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
+ // Long press down button for fast frame switching
+ showNextFrame();
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h
index f99056f22..53ffd2519 100644
--- a/src/graphics/ScreenFonts.h
+++ b/src/graphics/ScreenFonts.h
@@ -74,7 +74,7 @@
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
- defined(USE_ST7796)) && \
+ defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp
index 1645789a7..892285dcb 100644
--- a/src/graphics/SharedUIDisplay.cpp
+++ b/src/graphics/SharedUIDisplay.cpp
@@ -17,6 +17,12 @@ namespace graphics
void determineResolution(int16_t screenheight, int16_t screenwidth)
{
+
+#ifdef FORCE_LOW_RES
+ isHighResolution = false;
+ return;
+#endif
+
if (screenwidth > 128) {
isHighResolution = true;
}
@@ -24,11 +30,6 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
}
-
- // Special case for Heltec Wireless Tracker v1.1
- if (screenwidth == 160 && screenheight == 80) {
- isHighResolution = false;
- }
}
// === Shared External State ===
diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp
index 87593b0d4..12fac4f34 100644
--- a/src/graphics/TFTDisplay.cpp
+++ b/src/graphics/TFTDisplay.cpp
@@ -1,5 +1,6 @@
#include "configuration.h"
#include "main.h"
+#if USE_TFTDISPLAY
#if ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
@@ -123,6 +124,11 @@ static void rak14014_tpIntHandle(void)
_rak14014_touch_int = true;
}
+#elif defined(HACKADAY_COMMUNICATOR)
+#include
+Arduino_DataBus *bus = nullptr;
+Arduino_GFX *tft = nullptr;
+
#elif defined(ST72xx_DE)
#include
#include
@@ -1133,9 +1139,6 @@ static LGFX *tft = nullptr;
#endif
-#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \
- defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \
- (ARCH_PORTDUINO && HAS_SCREEN != 0)
#include "SPILock.h"
#include "TFTDisplay.h"
#include
@@ -1271,12 +1274,15 @@ void TFTDisplay::display(bool fromBlank)
x_LastPixelUpdate = x;
}
}
-
+#if defined(HACKADAY_COMMUNICATOR)
+ tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
+ (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
+#else
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
&linePixelBuffer[x_FirstPixelUpdate]);
-
+#endif
somethingChanged = true;
}
y++;
@@ -1340,6 +1346,8 @@ void TFTDisplay::sendCommand(uint8_t com)
display(true);
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
+#elif defined(HACKADAY_COMMUNICATOR)
+ tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup();
tft->powerSaveOff();
@@ -1352,7 +1360,8 @@ void TFTDisplay::sendCommand(uint8_t com)
unphone.backlight(true); // using unPhone library
#endif
#ifdef RAK14014
-#elif !defined(M5STACK) && !defined(ST7789_CS) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
+#elif !defined(M5STACK) && !defined(ST7789_CS) && \
+ !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172);
#endif
break;
@@ -1364,6 +1373,8 @@ void TFTDisplay::sendCommand(uint8_t com)
tft->clear();
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
+#elif defined(HACKADAY_COMMUNICATOR)
+ tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep();
tft->powerSaveOn();
@@ -1376,7 +1387,7 @@ void TFTDisplay::sendCommand(uint8_t com)
unphone.backlight(false); // using unPhone library
#endif
#ifdef RAK14014
-#elif !defined(M5STACK)
+#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(0);
#endif
break;
@@ -1392,7 +1403,7 @@ void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{
#ifdef RAK14014
// todo
-#else
+#elif !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(_brightness);
LOG_DEBUG("Brightness is set to value: %i ", _brightness);
#endif
@@ -1410,7 +1421,7 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
-#elif !defined(M5STACK)
+#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
return tft->touch() != nullptr;
#else
return false;
@@ -1429,7 +1440,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
-#elif !defined(M5STACK)
+#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
return tft->getTouch(x, y);
#else
return false;
@@ -1448,6 +1459,12 @@ bool TFTDisplay::connect()
LOG_INFO("Do TFT init");
#ifdef RAK14014
tft = new TFT_eSPI;
+#elif defined(HACKADAY_COMMUNICATOR)
+ bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
+ tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
+ 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
+ sizeof(nv3007_279_init_operations));
+
#else
tft = new LGFX;
#endif
@@ -1458,8 +1475,15 @@ bool TFTDisplay::connect()
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
#endif
-
+#ifdef HACKADAY_COMMUNICATOR
+ bool beginStatus = tft->begin();
+ if (beginStatus)
+ LOG_DEBUG("TFT Success!");
+ else
+ LOG_ERROR("TFT Fail!");
+#else
tft->init();
+#endif
#if defined(M5STACK)
tft->setRotation(0);
@@ -1492,4 +1516,4 @@ bool TFTDisplay::connect()
return true;
}
-#endif
+#endif // USE_TFTDISPLAY
diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp
index 6bccb1653..ceb3b83f5 100644
--- a/src/graphics/draw/DebugRenderer.cpp
+++ b/src/graphics/draw/DebugRenderer.cpp
@@ -97,8 +97,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
- defined(USE_ST7796) || \
- ARCH_PORTDUINO) && \
+ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
8, imgQuestionL1);
@@ -110,7 +109,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
#endif
} else {
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(USE_ST7796)) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
+ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
8, imgSFL1);
@@ -126,8 +126,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
// TODO: Raspberry Pi supports more than just the one screen size
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
- defined(USE_ST7796) || \
- ARCH_PORTDUINO) && \
+ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
imgInfoL1);
@@ -533,8 +532,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const int labelX = x;
int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK
+#ifndef T_DECK_PRO
barsOffset -= 12;
#endif
+#endif
#if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset;
#else
@@ -575,7 +576,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#endif
// Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT);
- display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
+ display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr);
};
// === Memory values ===
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index bd647c3d8..586bdd4a6 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -13,18 +13,48 @@
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h"
+#include "mesh/Default.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h"
#include "modules/TraceRouteModule.h"
+#include
+#include
#include
+#include
extern uint16_t TFT_MESH;
namespace graphics
{
+
+namespace
+{
+
+// Caller must ensure the provided options array outlives the banner callback.
+template
+BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N],
+ std::array &labels, Callback &&onSelection)
+{
+ for (size_t i = 0; i < N; ++i) {
+ labels[i] = options[i].label;
+ }
+
+ const MenuOption *optionsPtr = options;
+ auto callback = std::function &, int)>(std::forward(onSelection));
+
+ BannerOverlayOptions bannerOptions;
+ bannerOptions.message = message;
+ bannerOptions.optionsArrayPtr = labels.data();
+ bannerOptions.optionsCount = static_cast(N);
+ bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); };
+ return bannerOptions;
+}
+
+} // namespace
+
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
@@ -196,48 +226,38 @@ void menuHandler::DeviceRolePicker()
void menuHandler::RadioPresetPicker()
{
- static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
- "MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
- enum optionsNumbers {
- Back = 0,
- radiopreset_LongSlow = 1,
- radiopreset_LongModerate = 2,
- radiopreset_LongFast = 3,
- radiopreset_MediumSlow = 4,
- radiopreset_MediumFast = 5,
- radiopreset_ShortSlow = 6,
- radiopreset_ShortFast = 7,
- radiopreset_ShortTurbo = 8
- };
- BannerOverlayOptions bannerOptions;
- bannerOptions.message = "Radio Preset";
- bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 9;
- bannerOptions.bannerCallback = [](int selected) -> void {
- if (selected == Back) {
- menuHandler::menuQueue = menuHandler::lora_Menu;
- screen->runNow();
- return;
- } else if (selected == radiopreset_LongSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
- } else if (selected == radiopreset_LongModerate) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
- } else if (selected == radiopreset_LongFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
- } else if (selected == radiopreset_MediumSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
- } else if (selected == radiopreset_MediumFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
- } else if (selected == radiopreset_ShortSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
- } else if (selected == radiopreset_ShortFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
- } else if (selected == radiopreset_ShortTurbo) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
- }
- service->reloadConfig(SEGMENT_CONFIG);
- rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ static const RadioPresetOption presetOptions[] = {
+ {"Back", OptionsAction::Back},
+ {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO},
+ {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE},
+ {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST},
+ {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW},
+ {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST},
+ {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW},
+ {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST},
+ {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO},
};
+
+ constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]);
+ static std::array presetLabels{};
+
+ auto bannerOptions =
+ createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
+ if (option.action == OptionsAction::Back) {
+ menuHandler::menuQueue = menuHandler::lora_Menu;
+ screen->runNow();
+ return;
+ }
+
+ if (!option.hasValue) {
+ return;
+ }
+
+ config.lora.modem_preset = option.value;
+ service->reloadConfig(SEGMENT_CONFIG);
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ });
+
screen->showOverlayBanner(bannerOptions);
}
@@ -576,7 +596,7 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu()
{
- enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
+ enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
@@ -592,6 +612,10 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Bluetooth Toggle";
#endif
optionsEnumArray[options++] = Bluetooth;
+#if HAS_WIFI && !defined(ARCH_PORTDUINO)
+ optionsArray[options] = "WiFi Toggle";
+ optionsEnumArray[options++] = WiFiToggle;
+#endif
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Power";
#else
@@ -629,6 +653,11 @@ void menuHandler::systemBaseMenu()
} else if (selected == Bluetooth) {
menuQueue = bluetooth_toggle_menu;
screen->runNow();
+#if HAS_WIFI && !defined(ARCH_PORTDUINO)
+ } else if (selected == WiFiToggle) {
+ menuQueue = wifi_toggle_menu;
+ screen->runNow();
+#endif
} else if (selected == Back && !test_enabled) {
test_count++;
if (test_count > 4) {
@@ -1031,14 +1060,16 @@ void menuHandler::switchToMUIMenu()
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{
- static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal",
- "Pink", "White"};
+ static const char *optionsArray[] = {
+ "Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Blue", "Teal", "Cyan", "Ice", "Pink",
+ "White", "Gray"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Screen Color";
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 10;
+ bannerOptions.optionsCount = 14;
bannerOptions.bannerCallback = [display](int selected) -> void {
-#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
+ HAS_TFT || defined(HACKADAY_COMMUNICATOR)
uint8_t TFT_MESH_r = 0;
uint8_t TFT_MESH_g = 0;
uint8_t TFT_MESH_b = 0;
@@ -1071,20 +1102,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
TFT_MESH_g = 153;
TFT_MESH_b = 255;
} else if (selected == 7) {
- LOG_INFO("Setting color to Teal");
- TFT_MESH_r = 64;
- TFT_MESH_g = 224;
- TFT_MESH_b = 208;
+ LOG_INFO("Setting color to Blue");
+ TFT_MESH_r = 0;
+ TFT_MESH_g = 0;
+ TFT_MESH_b = 255;
} else if (selected == 8) {
+ LOG_INFO("Setting color to Teal");
+ TFT_MESH_r = 16;
+ TFT_MESH_g = 102;
+ TFT_MESH_b = 102;
+ } else if (selected == 9) {
+ LOG_INFO("Setting color to Cyan");
+ TFT_MESH_r = 0;
+ TFT_MESH_g = 255;
+ TFT_MESH_b = 255;
+ } else if (selected == 10) {
+ LOG_INFO("Setting color to Ice");
+ TFT_MESH_r = 173;
+ TFT_MESH_g = 216;
+ TFT_MESH_b = 230;
+ } else if (selected == 11) {
LOG_INFO("Setting color to Pink");
TFT_MESH_r = 255;
TFT_MESH_g = 105;
TFT_MESH_b = 180;
- } else if (selected == 9) {
+ } else if (selected == 12) {
LOG_INFO("Setting color to White");
TFT_MESH_r = 255;
TFT_MESH_g = 255;
TFT_MESH_b = 255;
+ } else if (selected == 13) {
+ LOG_INFO("Setting color to Gray");
+ TFT_MESH_r = 128;
+ TFT_MESH_g = 128;
+ TFT_MESH_b = 128;
} else {
menuQueue = system_base_menu;
screen->runNow();
@@ -1278,19 +1329,28 @@ void menuHandler::wifiBaseMenu()
void menuHandler::wifiToggleMenu()
{
- enum optionsNumbers { Back, Wifi_toggle };
+ enum optionsNumbers { Back, Wifi_disable, Wifi_enable };
- static const char *optionsArray[] = {"Back", "Disable"};
+ static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"};
BannerOverlayOptions bannerOptions;
- bannerOptions.message = "Disable Wifi and\nEnable Bluetooth?";
+ bannerOptions.message = "WiFi Actions";
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 2;
+ bannerOptions.optionsCount = 3;
+ if (config.network.wifi_enabled == true)
+ bannerOptions.InitialSelected = 2;
+ else
+ bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void {
- if (selected == Wifi_toggle) {
+ if (selected == Wifi_disable) {
config.network.wifi_enabled = false;
config.bluetooth.enabled = true;
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ } else if (selected == Wifi_enable) {
+ config.network.wifi_enabled = true;
+ config.bluetooth.enabled = false;
+ service->reloadConfig(SEGMENT_CONFIG);
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
};
screen->showOverlayBanner(bannerOptions);
@@ -1338,7 +1398,7 @@ void menuHandler::screenOptionsMenu()
static int optionsEnumArray[5] = {Back};
int options = 1;
-#if defined(T_DECK) || defined(T_LORA_PAGER)
+#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
@@ -1350,7 +1410,8 @@ void menuHandler::screenOptionsMenu()
}
// Only show screen color for TFT displays
-#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
+#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
+ HAS_TFT || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor;
#endif
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
index a611b7c9d..df7c2739b 100644
--- a/src/graphics/draw/MenuHandler.h
+++ b/src/graphics/draw/MenuHandler.h
@@ -99,5 +99,24 @@ class menuHandler
static void BluetoothToggleMenu();
};
+/* Generic Menu Options designations */
+enum class OptionsAction { Back, Select };
+
+template struct MenuOption {
+ const char *label;
+ OptionsAction action;
+ bool hasValue;
+ T value;
+
+ MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn)
+ : label(labelIn), action(actionIn), hasValue(true), value(valueIn)
+ {
+ }
+
+ MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
+};
+
+using RadioPresetOption = MenuOption;
+
} // namespace graphics
#endif
\ No newline at end of file
diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp
index 26bfe8447..e95cc1610 100644
--- a/src/graphics/draw/NotificationRenderer.cpp
+++ b/src/graphics/draw/NotificationRenderer.cpp
@@ -85,9 +85,13 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::resetBanner()
{
+ notificationTypeEnum previousType = current_notification_type;
+
alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none;
+ OnScreenKeyboardModule::instance().clearPopup();
+
inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0;
curSelected = 0;
@@ -100,6 +104,13 @@ void NotificationRenderer::resetBanner()
currentNumber = 0;
nodeDB->pause_sort(false);
+
+ // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
+ // to ensure any messages received during keyboard use are now displayed
+ if (previousType == notificationTypeEnum::text_input && screen) {
+ OnScreenKeyboardModule::instance().stop(false);
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ }
}
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
@@ -163,13 +174,15 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
// modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
// Handle input
- if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
+ if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
+ inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
if (this_digit == 9) {
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
currentNumber += (pow_of_10(numDigits - curSelected - 1));
}
- } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
+ } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
+ inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
if (this_digit == 0) {
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
} else {
@@ -251,10 +264,10 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
// Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
- inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
+ inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
- inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
+ inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum);
@@ -368,10 +381,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input
if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
- inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
+ inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
- inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
+ inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) {
@@ -769,40 +782,8 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
}
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
- if (inEvent.inputEvent == INPUT_BROKER_UP) {
- // high frequency for move cursor left/right than up/down with encoders
- extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
- extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
- if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
- virtualKeyboard->moveCursorLeft();
- } else {
- virtualKeyboard->moveCursorUp();
- }
- } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
- extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
- extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
- if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
- virtualKeyboard->moveCursorRight();
- } else {
- virtualKeyboard->moveCursorDown();
- }
- } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
- virtualKeyboard->moveCursorLeft();
- } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
- virtualKeyboard->moveCursorRight();
- } else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
- virtualKeyboard->moveCursorUp();
- } else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
- virtualKeyboard->moveCursorDown();
- } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
- virtualKeyboard->moveCursorLeft();
- } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
- virtualKeyboard->moveCursorRight();
- } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
- virtualKeyboard->handlePress();
- } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
- virtualKeyboard->handleLongPress();
- } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
+ bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard);
+ if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback;
delete virtualKeyboard;
virtualKeyboard = nullptr;
@@ -821,12 +802,28 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
inEvent.inputEvent = INPUT_BROKER_NONE;
}
+ // Re-check pointer before drawing to avoid use-after-free and crashes
+ if (!virtualKeyboard) {
+ // Ensure we exit text_input state and restore frames
+ if (current_notification_type == notificationTypeEnum::text_input) {
+ resetBanner();
+ }
+ if (screen) {
+ screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
+ }
+ // If screen is null, do nothing (safe fallback)
+ return;
+ }
+
// Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
// Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0);
+
+ // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule
+ OnScreenKeyboardModule::instance().drawPopupOverlay(display);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
@@ -839,5 +836,12 @@ bool NotificationRenderer::isOverlayBannerShowing()
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
}
+void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
+{
+ if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
+ return;
+ OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
+}
+
} // namespace graphics
#endif
\ No newline at end of file
diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h
index edb069513..e51bfa5ab 100644
--- a/src/graphics/draw/NotificationRenderer.h
+++ b/src/graphics/draw/NotificationRenderer.h
@@ -4,6 +4,7 @@
#include "OLEDDisplayUi.h"
#include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h"
+#include "modules/OnScreenKeyboardModule.h"
#include
#include
#define MAX_LINES 5
@@ -31,6 +32,7 @@ class NotificationRenderer
static bool pauseBanner;
static void resetBanner();
+ static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index 3d23acc9f..1f01640bf 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -257,7 +257,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
}
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || defined(USE_ST7796)) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
+ defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) {
diff --git a/src/graphics/images.h b/src/graphics/images.h
index 998fe8e2a..c268b3269 100644
--- a/src/graphics/images.h
+++ b/src/graphics/images.h
@@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
- defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(USE_ST7796) || defined(ST7796_CS) || ARCH_PORTDUINO) && \
+ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
+ defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp
new file mode 100644
index 000000000..87c8a24ae
--- /dev/null
+++ b/src/input/HackadayCommunicatorKeyboard.cpp
@@ -0,0 +1,217 @@
+#if defined(HACKADAY_COMMUNICATOR)
+
+#include "HackadayCommunicatorKeyboard.h"
+#include "main.h"
+
+#define _TCA8418_COLS 10
+#define _TCA8418_ROWS 8
+#define _TCA8418_NUM_KEYS 80
+
+#define _TCA8418_MULTI_TAP_THRESHOLD 1500
+
+using Key = TCA8418KeyboardBase::TCA8418Key;
+
+constexpr uint8_t modifierRightShiftKey = 30;
+constexpr uint8_t modifierRightShift = 0b0001;
+constexpr uint8_t modifierLeftShiftKey = 76; // keynum -1
+constexpr uint8_t modifierLeftShift = 0b0001;
+// constexpr uint8_t modifierSymKey = 42;
+// constexpr uint8_t modifierSym = 0b0010;
+
+// Num chars per key, Modulus for rotating through characters
+static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
+};
+
+static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
+ {},
+ {'+'},
+ {'9'},
+ {'8'},
+ {'7'},
+ {'2'},
+ {'3'},
+ {'4'},
+ {'5'},
+ {Key::ESC},
+ {'q', 'Q'},
+ {'w', 'W'},
+ {'e', 'E'},
+ {'r', 'R'},
+ {'t', 'T'},
+ {'y', 'Y'},
+ {'u', 'U'},
+ {'i', 'I'},
+ {'o', 'O'},
+ {Key::TAB},
+ {'a', 'A'},
+ {'s', 'S'},
+ {'d', 'D'},
+ {'f', 'F'},
+ {'g', 'G'},
+ {'h', 'H'},
+ {'j', 'J'},
+ {'k', 'K'},
+ {'l', 'L'},
+ {},
+ {'z', 'Z'},
+ {'x', 'X'},
+ {'c', 'C'},
+ {'v', 'V'},
+ {'b', 'B'},
+ {'n', 'N'},
+ {'m', 'M'},
+ {',', '<'},
+ {'.', '>'},
+ {},
+ {},
+ {},
+ {'\\'},
+ {' '},
+ {},
+ {Key::RIGHT},
+ {Key::DOWN},
+ {Key::LEFT},
+ {},
+ {},
+ {},
+ {'-'},
+ {'6', '^'},
+ {'5', '%'},
+ {'4', '$'},
+ {'[', '{'},
+ {']', '}'},
+ {'p', 'P'},
+ {},
+ {},
+ {},
+ {'*'},
+ {'3', '#'},
+ {'2', '@'},
+ {'1', '!'},
+ {Key::SELECT},
+ {'\'', '"'},
+ {';', ':'},
+ {},
+ {},
+ {},
+ {'/', '?'},
+ {'='},
+ {'.', '>'},
+ {'0', ')'},
+ {},
+ {Key::UP},
+ {Key::BSP},
+ {}};
+
+HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
+ : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
+ last_tap(0L), char_idx(0), tap_interval(0)
+{
+ reset();
+}
+
+void HackadayCommunicatorKeyboard::reset(void)
+{
+ TCA8418KeyboardBase::reset();
+ enableInterrupts();
+}
+
+// handle multi-key presses (shift and alt)
+void HackadayCommunicatorKeyboard::trigger()
+{
+ uint8_t count = keyCount();
+ if (count == 0)
+ return;
+ for (uint8_t i = 0; i < count; ++i) {
+ uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
+ uint8_t key = k & 0x7F;
+ if (k & 0x80) {
+ pressed(key);
+ } else {
+ released();
+ state = Idle;
+ }
+ }
+}
+
+void HackadayCommunicatorKeyboard::pressed(uint8_t key)
+{
+ if (state == Init || state == Busy) {
+ return;
+ }
+
+ if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
+ modifierFlag = 0;
+ }
+
+ uint8_t next_key = 0;
+ int row = (key - 1) / 10;
+ int col = (key - 1) % 10;
+ if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
+ return; // Invalid key
+ }
+
+ next_key = row * _TCA8418_COLS + col;
+ state = Held;
+
+ uint32_t now = millis();
+ tap_interval = now - last_tap;
+
+ updateModifierFlag(next_key);
+ if (isModifierKey(next_key)) {
+ last_modifier_time = now;
+ }
+
+ if (tap_interval < 0) {
+ last_tap = 0;
+ state = Busy;
+ return;
+ }
+
+ if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
+ char_idx = 0;
+ } else {
+ char_idx += 1;
+ }
+
+ last_key = next_key;
+ last_tap = now;
+}
+
+void HackadayCommunicatorKeyboard::released()
+{
+ if (state != Held) {
+ return;
+ }
+
+ if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
+ last_key = -1;
+ state = Idle;
+ return;
+ }
+
+ uint32_t now = millis();
+ last_tap = now;
+ if (HackadayCommunicatorTapMod[last_key])
+ queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]);
+ if (isModifierKey(last_key) == false)
+ modifierFlag = 0;
+}
+
+void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key)
+{
+ if (key == modifierRightShiftKey) {
+ modifierFlag ^= modifierRightShift;
+ } else if (key == modifierLeftShiftKey) {
+ modifierFlag ^= modifierLeftShift;
+ }
+}
+
+bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key)
+{
+ return (key == modifierRightShiftKey || key == modifierLeftShiftKey);
+}
+
+#endif
\ No newline at end of file
diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h
new file mode 100644
index 000000000..8316bed72
--- /dev/null
+++ b/src/input/HackadayCommunicatorKeyboard.h
@@ -0,0 +1,26 @@
+#include "TCA8418KeyboardBase.h"
+
+class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
+{
+ public:
+ HackadayCommunicatorKeyboard();
+ void reset(void);
+ void trigger(void) override;
+ virtual ~HackadayCommunicatorKeyboard() {}
+
+ protected:
+ void pressed(uint8_t key) override;
+ void released(void) override;
+
+ void updateModifierFlag(uint8_t key);
+ bool isModifierKey(uint8_t key);
+
+ private:
+ uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
+ uint32_t last_modifier_time; // Timestamp of the last modifier key press
+ int8_t last_key;
+ int8_t next_key;
+ uint32_t last_tap;
+ uint8_t char_idx;
+ int32_t tap_interval;
+};
diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h
index 36328ca64..c55d7fa53 100644
--- a/src/input/InputBroker.h
+++ b/src/input/InputBroker.h
@@ -3,6 +3,12 @@
#include "Observer.h"
#include "freertosinc.h"
+#ifdef InputBrokerDebug
+#define LOG_INPUT(...) LOG_DEBUG(__VA_ARGS__)
+#else
+#define LOG_INPUT(...)
+#endif
+
enum input_broker_event {
INPUT_BROKER_NONE = 0,
INPUT_BROKER_SELECT = 10,
@@ -47,6 +53,7 @@ typedef struct _InputEvent {
class InputPollable
{
public:
+ virtual ~InputPollable() = default;
virtual void pollOnce() = 0;
};
diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp
index 7b43fa256..cc1222595 100644
--- a/src/input/RotaryEncoderImpl.cpp
+++ b/src/input/RotaryEncoderImpl.cpp
@@ -3,6 +3,9 @@
#include "RotaryEncoderImpl.h"
#include "InputBroker.h"
#include "RotaryEncoder.h"
+#ifdef ARCH_ESP32
+#include "sleep.h"
+#endif
#define ORIGIN_NAME "RotaryEncoder"
@@ -11,6 +14,20 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl()
{
rotary = nullptr;
+#ifdef ARCH_ESP32
+ isFirstInit = true;
+#endif
+}
+
+RotaryEncoderImpl::~RotaryEncoderImpl()
+{
+ LOG_DEBUG("RotaryEncoderImpl destructor");
+ detachRotaryEncoderInterrupts();
+
+ if (rotary != nullptr) {
+ delete rotary;
+ rotary = nullptr;
+ }
}
bool RotaryEncoderImpl::init()
@@ -25,15 +42,22 @@ bool RotaryEncoderImpl::init()
eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw);
eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press);
- rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
- moduleConfig.canned_message.inputbroker_pin_press);
- rotary->resetButton();
+ if (rotary == nullptr) {
+ rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
+ moduleConfig.canned_message.inputbroker_pin_press);
+ }
- interruptInstance = this;
- auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
+ attachRotaryEncoderInterrupts();
+
+#ifdef ARCH_ESP32
+ // Register callbacks for before and after lightsleep
+ // Used to detach and reattach interrupts
+ if (isFirstInit) {
+ lsObserver.observe(¬ifyLightSleep);
+ lsEndObserver.observe(¬ifyLightSleepEnd);
+ isFirstInit = false;
+ }
+#endif
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
@@ -71,6 +95,50 @@ void RotaryEncoderImpl::pollOnce()
}
}
+void RotaryEncoderImpl::detachRotaryEncoderInterrupts()
+{
+ LOG_DEBUG("RotaryEncoderImpl detach button interrupts");
+ if (interruptInstance == this) {
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a);
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b);
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press);
+ interruptInstance = nullptr;
+ } else {
+ LOG_WARN("RotaryEncoderImpl: interrupts already detached");
+ }
+}
+
+void RotaryEncoderImpl::attachRotaryEncoderInterrupts()
+{
+ LOG_DEBUG("RotaryEncoderImpl attach button interrupts");
+ if (rotary != nullptr && interruptInstance == nullptr) {
+ rotary->resetButton();
+
+ interruptInstance = this;
+ auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
+ } else {
+ LOG_WARN("RotaryEncoderImpl: interrupts already attached");
+ }
+}
+
+#ifdef ARCH_ESP32
+
+int RotaryEncoderImpl::beforeLightSleep(void *unused)
+{
+ detachRotaryEncoderInterrupts();
+ return 0; // Indicates success;
+}
+
+int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause)
+{
+ attachRotaryEncoderInterrupts();
+ return 0; // Indicates success;
+}
+#endif
+
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
#endif
\ No newline at end of file
diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h
index 6f8e9fe5f..ec8a064bd 100644
--- a/src/input/RotaryEncoderImpl.h
+++ b/src/input/RotaryEncoderImpl.h
@@ -8,12 +8,18 @@
class RotaryEncoder;
-class RotaryEncoderImpl : public InputPollable
+class RotaryEncoderImpl final : public InputPollable
{
public:
RotaryEncoderImpl();
- bool init(void);
+ ~RotaryEncoderImpl() override;
+ bool init();
virtual void pollOnce() override;
+ // Disconnect and reconnect interrupts for light sleep
+#ifdef ARCH_ESP32
+ int beforeLightSleep(void *unused);
+ int afterLightSleep(esp_sleep_wakeup_cause_t cause);
+#endif
protected:
static RotaryEncoderImpl *interruptInstance;
@@ -23,6 +29,21 @@ class RotaryEncoderImpl : public InputPollable
input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary;
+
+ private:
+#ifdef ARCH_ESP32
+ bool isFirstInit;
+#endif
+ void detachRotaryEncoderInterrupts();
+ void attachRotaryEncoderInterrupts();
+
+#ifdef ARCH_ESP32
+ // Get notified when lightsleep begins and ends
+ CallbackObserver lsObserver =
+ CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep);
+ CallbackObserver lsEndObserver =
+ CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep);
+#endif
};
extern RotaryEncoderImpl *rotaryEncoderImpl;
diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp
index 12cbc36fb..1da2ea008 100644
--- a/src/input/RotaryEncoderInterruptImpl1.cpp
+++ b/src/input/RotaryEncoderInterruptImpl1.cpp
@@ -27,7 +27,9 @@ bool RotaryEncoderInterruptImpl1::init()
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
+#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
+#endif
return true;
}
diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp
index 2df1ace70..a5d2c614f 100644
--- a/src/input/SerialKeyboard.cpp
+++ b/src/input/SerialKeyboard.cpp
@@ -2,6 +2,8 @@
#include "configuration.h"
#include
+SerialKeyboard *globalSerialKeyboard = nullptr;
+
#ifdef INPUTBROKER_SERIAL_TYPE
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
@@ -25,6 +27,8 @@ unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w',
SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
{
this->_originName = name;
+
+ globalSerialKeyboard = this;
}
void SerialKeyboard::erase()
@@ -85,9 +89,21 @@ int32_t SerialKeyboard::runOnce()
e.source = this->_originName;
// SELECT OR SEND OR CANCEL EVENT
if (!(shiftRegister2 & (1 << 3))) {
- e.inputEvent = INPUT_BROKER_UP;
+ if (shift > 0) {
+ e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED
+ e.kbchar = 0x09; // TAB
+ shift = 0; // reset shift after TAB
+ } else {
+ e.inputEvent = INPUT_BROKER_LEFT;
+ }
} else if (!(shiftRegister2 & (1 << 2))) {
- e.inputEvent = INPUT_BROKER_RIGHT;
+ if (shift > 0) {
+ e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED
+ e.kbchar = 0x09; // TAB
+ shift = 0; // reset shift after TAB
+ } else {
+ e.inputEvent = INPUT_BROKER_RIGHT;
+ }
e.kbchar = 0;
} else if (!(shiftRegister2 & (1 << 1))) {
e.inputEvent = INPUT_BROKER_SELECT;
diff --git a/src/input/SerialKeyboard.h b/src/input/SerialKeyboard.h
index 1480c4d58..f25eb2630 100644
--- a/src/input/SerialKeyboard.h
+++ b/src/input/SerialKeyboard.h
@@ -8,6 +8,8 @@ class SerialKeyboard : public Observable, public concurrency
public:
explicit SerialKeyboard(const char *name);
+ uint8_t getShift() const { return shift; }
+
protected:
virtual int32_t runOnce() override;
void erase();
@@ -22,4 +24,6 @@ class SerialKeyboard : public Observable, public concurrency
int lastKeyPressed = 13;
int quickPress = 0;
unsigned long lastPressTime = 0;
-};
\ No newline at end of file
+};
+
+extern SerialKeyboard *globalSerialKeyboard;
\ No newline at end of file
diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp
index 4ddaf7064..bbd07e199 100644
--- a/src/input/TrackballInterruptBase.cpp
+++ b/src/input/TrackballInterruptBase.cpp
@@ -45,7 +45,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
this->_pinLeft, this->_pinRight, pinPress);
+#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
+#endif
this->setInterval(100);
}
@@ -88,6 +90,50 @@ int32_t TrackballInterruptBase::runOnce()
}
}
+ if (directionDetected && directionStartTime > 0) {
+ uint32_t directionDuration = millis() - directionStartTime;
+ uint8_t directionPressedNow = 0;
+ directionInterval++;
+
+ if (!digitalRead(_pinUp)) {
+ directionPressedNow = TB_ACTION_UP;
+ } else if (!digitalRead(_pinDown)) {
+ directionPressedNow = TB_ACTION_DOWN;
+ } else if (!digitalRead(_pinLeft)) {
+ directionPressedNow = TB_ACTION_LEFT;
+ } else if (!digitalRead(_pinRight)) {
+ directionPressedNow = TB_ACTION_RIGHT;
+ }
+
+ const uint8_t DIRECTION_REPEAT_THRESHOLD = 3;
+
+ if (directionPressedNow == TB_ACTION_NONE) {
+ // Reset state
+ directionDetected = false;
+ directionStartTime = 0;
+ directionInterval = 0;
+ this->action = TB_ACTION_NONE;
+ } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) {
+ // repeat event when long press these direction.
+ switch (directionPressedNow) {
+ case TB_ACTION_UP:
+ e.inputEvent = this->_eventUp;
+ break;
+ case TB_ACTION_DOWN:
+ e.inputEvent = this->_eventDown;
+ break;
+ case TB_ACTION_LEFT:
+ e.inputEvent = this->_eventLeft;
+ break;
+ case TB_ACTION_RIGHT:
+ e.inputEvent = this->_eventRight;
+ break;
+ }
+
+ directionInterval = 0;
+ }
+ }
+
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection
@@ -113,17 +159,22 @@ int32_t TrackballInterruptBase::runOnce()
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
- } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
- // LOG_DEBUG("Trackball event UP");
+ } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) {
+ directionDetected = true;
+ directionStartTime = millis();
e.inputEvent = this->_eventUp;
- } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) {
- // LOG_DEBUG("Trackball event DOWN");
+ // send event first,will automatically trigger every 50ms * 3 after 500ms
+ } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) {
+ directionDetected = true;
+ directionStartTime = millis();
e.inputEvent = this->_eventDown;
- } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) {
- // LOG_DEBUG("Trackball event LEFT");
+ } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) {
+ directionDetected = true;
+ directionStartTime = millis();
e.inputEvent = this->_eventLeft;
- } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) {
- // LOG_DEBUG("Trackball event RIGHT");
+ } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) {
+ directionDetected = true;
+ directionStartTime = millis();
e.inputEvent = this->_eventRight;
}
#endif
diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h
index 76a99f33d..67d4ee449 100644
--- a/src/input/TrackballInterruptBase.h
+++ b/src/input/TrackballInterruptBase.h
@@ -49,10 +49,14 @@ class TrackballInterruptBase : public Observable, public con
// Long press detection for press button
uint32_t pressStartTime = 0;
+ uint32_t directionStartTime = 0;
+ uint8_t directionInterval = 0;
bool pressDetected = false;
+ bool directionDetected = false;
uint32_t lastLongPressEventTime = 0;
+ uint32_t lastDirectionPressEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 500; // ms
- static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events
+ static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events
private:
input_broker_event _eventDown = INPUT_BROKER_NONE;
diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h
index ae84efdaf..2b9d38c83 100644
--- a/src/input/UpDownInterruptBase.h
+++ b/src/input/UpDownInterruptBase.h
@@ -3,6 +3,14 @@
#include "InputBroker.h"
#include "mesh/NodeDB.h"
+#ifndef UPDOWN_LONG_PRESS_DURATION
+#define UPDOWN_LONG_PRESS_DURATION 300
+#endif
+
+#ifndef UPDOWN_LONG_PRESS_REPEAT_INTERVAL
+#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300
+#endif
+
class UpDownInterruptBase : public Observable, public concurrency::OSThread
{
public:
@@ -40,8 +48,8 @@ class UpDownInterruptBase : public Observable, public concur
uint32_t lastPressLongEventTime = 0;
uint32_t lastUpLongEventTime = 0;
uint32_t lastDownLongEventTime = 0;
- static const uint32_t LONG_PRESS_DURATION = 300;
- static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300;
+ static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION;
+ static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL;
private:
uint8_t _pinDown = 0;
diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp
index 9b0b1f39e..906dcd2a8 100644
--- a/src/input/UpDownInterruptImpl1.cpp
+++ b/src/input/UpDownInterruptImpl1.cpp
@@ -29,7 +29,9 @@ bool UpDownInterruptImpl1::init()
eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp,
UpDownInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
+#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
+#endif
return true;
}
diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp
index 0ed2df116..0085c806b 100644
--- a/src/input/kbI2cBase.cpp
+++ b/src/input/kbI2cBase.cpp
@@ -7,6 +7,8 @@
#include "TDeckProKeyboard.h"
#elif defined(T_LORA_PAGER)
#include "TLoraPagerKeyboard.h"
+#elif defined(HACKADAY_COMMUNICATOR)
+#include "HackadayCommunicatorKeyboard.h"
#else
#include "TCA8418Keyboard.h"
#endif
@@ -20,6 +22,8 @@ KbI2cBase::KbI2cBase(const char *name)
TCAKeyboard(*(new TDeckProKeyboard()))
#elif defined(T_LORA_PAGER)
TCAKeyboard(*(new TLoraPagerKeyboard()))
+#elif defined(HACKADAY_COMMUNICATOR)
+ TCAKeyboard(*(new HackadayCommunicatorKeyboard()))
#else
TCAKeyboard(*(new TCA8418Keyboard()))
#endif
@@ -328,7 +332,7 @@ int32_t KbI2cBase::runOnce()
break;
}
if (e.inputEvent != INPUT_BROKER_NONE) {
- LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar);
+ // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar);
this->notifyObservers(&e);
}
TCAKeyboard.trigger();
diff --git a/src/main.cpp b/src/main.cpp
index 0b821ca26..87dbdfe14 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -400,7 +400,10 @@ void setup()
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(BOARD_BL_EN, OUTPUT);
+#elif defined(HACKADAY_COMMUNICATOR)
+ pinMode(KB_INT, INPUT);
#endif
+
concurrency::hasBeenSetup = true;
#if ARCH_PORTDUINO
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
@@ -442,6 +445,13 @@ void setup()
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
+#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
+#ifndef SENSECAP_INDICATOR
+ // use PSRAM for malloc calls > 256 bytes
+ heap_caps_malloc_extmem_enable(256);
+#endif
+#endif
+
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
@@ -884,7 +894,7 @@ void setup()
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
- defined(USE_ST7796) || defined(USE_SPISSD1306)
+ defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
#elif defined(ARCH_PORTDUINO)
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
@@ -1161,7 +1171,7 @@ void setup()
// the current region name)
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
- defined(USE_ST7796) || defined(USE_SPISSD1306)
+ defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR)
if (screen)
screen->setup();
#elif defined(ARCH_PORTDUINO)
@@ -1454,8 +1464,10 @@ void setup()
#endif
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
+#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
#endif
+#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
// Start web server thread.
diff --git a/src/mesh/Default.h b/src/mesh/Default.h
index 218d8d0fb..a60e3af9b 100644
--- a/src/mesh/Default.h
+++ b/src/mesh/Default.h
@@ -57,14 +57,7 @@ class Default
// Note: Kept as uint32_t to match the public API parameter type
static float congestionScalingCoefficient(uint32_t numOnlineNodes)
{
- // Increase frequency of broadcasts for small networks regardless of preset
- if (numOnlineNodes <= 10) {
- return 0.6;
- } else if (numOnlineNodes <= 20) {
- return 0.7;
- } else if (numOnlineNodes <= 30) {
- return 0.8;
- } else if (numOnlineNodes <= 40) {
+ if (numOnlineNodes <= 40) {
return 1.0;
} else {
float throttlingFactor = 0.075;
diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp
index 032be241b..b7459abe0 100644
--- a/src/mesh/FloodingRouter.cpp
+++ b/src/mesh/FloodingRouter.cpp
@@ -124,6 +124,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
}
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB &&
+ nodeDB->isFromOrToFavoritedNode(*p)) {
+ iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
+ }
}
bool FloodingRouter::isRebroadcaster()
diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h
index eda3f8881..e7178bcfe 100644
--- a/src/mesh/MeshModule.h
+++ b/src/mesh/MeshModule.h
@@ -225,4 +225,4 @@ class MeshModule
/** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet
* This ensures that if the request packet was sent reliably, the reply is sent that way as well.
*/
-void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);
\ No newline at end of file
+void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 1b2af082d..297404747 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -276,6 +276,10 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
if (nodeDB->hasValidPosition(node)) {
#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS
if (positionModule) {
+ if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) {
+ LOG_DEBUG("Skip position ping; no fresh position since boot");
+ return false;
+ }
LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
positionModule->sendOurPosition(dest, wantReplies, node->channel);
return true;
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index 4e99a22ef..9052ee17c 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -664,7 +664,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.bluetooth.fixed_pin = defaultBLEPin;
#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
- defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || defined(USE_ST7796)
+ defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \
+ defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
bool hasScreen = true;
#ifdef HELTEC_MESH_NODE_T114
uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET);
@@ -804,11 +805,16 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
-#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312)
- // Default to RAK led pin 2 (blue)
+#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
+ defined(ELECROW_ThinkNode_M6)
+ // Default to PIN_LED2 for external notification output (LED color depends on device variant)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2;
+#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
+ moduleConfig.external_notification.active = false;
+#else
moduleConfig.external_notification.active = true;
+#endif
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
@@ -1038,6 +1044,7 @@ void NodeDB::clearLocalPosition()
node->position.altitude = 0;
node->position.time = 0;
setLocalPosition(meshtastic_Position_init_default);
+ localPositionUpdatedSinceBoot = false;
}
void NodeDB::cleanupMeshDB()
diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h
index 306acc0a5..6fd8deb87 100644
--- a/src/mesh/NodeDB.h
+++ b/src/mesh/NodeDB.h
@@ -279,9 +279,13 @@ class NodeDB
LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i,
position.time, position.timestamp);
localPosition = position;
+ if (position.latitude_i != 0 || position.longitude_i != 0) {
+ localPositionUpdatedSinceBoot = true;
+ }
}
bool hasValidPosition(const meshtastic_NodeInfoLite *n);
+ bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; }
#if !defined(MESHTASTIC_EXCLUDE_PKI)
bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest);
@@ -301,6 +305,7 @@ class NodeDB
private:
bool duplicateWarned = false;
+ bool localPositionUpdatedSinceBoot = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
uint32_t lastSort = 0; // When last sorted the nodeDB
diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp
index 3c0da4494..f7daf1122 100644
--- a/src/mesh/RadioInterface.cpp
+++ b/src/mesh/RadioInterface.cpp
@@ -296,11 +296,6 @@ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p)
return true;
}
- // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early
- if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
- return nodeDB->isFromOrToFavoritedNode(*p);
- }
-
return false;
}
@@ -503,6 +498,11 @@ void RadioInterface::applyModemConfig()
cr = 5;
sf = 10;
break;
+ case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
+ bw = (myRegion->wideLora) ? 1625.0 : 500;
+ cr = 8;
+ sf = 11;
+ break;
default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal.
bw = (myRegion->wideLora) ? 812.5 : 250;
cr = 5;
@@ -539,13 +539,26 @@ void RadioInterface::applyModemConfig()
}
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
- static const char *err_string = "Regional frequency range is smaller than bandwidth. Fall back to default preset";
- LOG_ERROR(err_string);
+ const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f;
+ const float requestedBwKHz = bw;
+ const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset
+ const char *presetName =
+ DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
+
+ char err_string[160];
+ if (isWideRequest) {
+ snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.",
+ myRegion->name, presetName);
+ } else {
+ snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.",
+ myRegion->name, regionSpanKHz, requestedBwKHz);
+ }
+ LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
- sprintf(cn->message, err_string);
+ snprintf(cn->message, sizeof(cn->message), "%s", err_string);
service->sendClientNotification(cn);
// Set to default modem preset
diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp
index 00066a7a3..7619fc106 100644
--- a/src/mesh/ReliableRouter.cpp
+++ b/src/mesh/ReliableRouter.cpp
@@ -150,7 +150,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0;
// We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records
- if (ackId || nakId) {
+ if ((ackId || nakId) &&
+ // Implicit ACKs from MQTT should not stop retransmissions
+ !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) {
LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId);
if (ackId) {
stopRetransmission(p->to, ackId);
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 05f47d7f4..ad0c0be6f 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -37,8 +37,8 @@
static MemoryDynamic dynamicPool;
Allocator &packetPool = dynamicPool;
-#elif defined(ARCH_STM32WL)
-// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically.
+#elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM)
+// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically.
// For now, make it dynamic again.
#define MAX_PACKETS \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
@@ -526,6 +526,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
#elif ARCH_PORTDUINO
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
+ } else if (portduino_config.JSONFilename != "") {
+ if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) {
+ JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl;
+ }
}
#endif
return DecodeState::DECODE_SUCCESS;
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index 327568316..d4ef5bee4 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -293,7 +293,8 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Long Range - Fast */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0,
- /* Long Range - Slow */
+ /* Long Range - Slow
+ Deprecated in 2.7: Unpopular slow preset. */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1,
/* Very Long Range - Slow
Deprecated in 2.5: Works only with txco and is unusably slow */
@@ -311,7 +312,10 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Short Range - Turbo
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth. */
- meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8
+ meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
+ /* Long Range - Turbo
+ This preset performs similarly to LongFast, but with 500Khz bandwidth. */
+ meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
} meshtastic_Config_LoRaConfig_ModemPreset;
typedef enum _meshtastic_Config_BluetoothConfig_PairingMode {
@@ -689,8 +693,8 @@ extern "C" {
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+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
-#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1))
+#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
+#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
#define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN
#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index aa510a86d..5f0c27fff 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -417,6 +417,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
}
case meshtastic_AdminMessage_enter_dfu_mode_request_tag: {
LOG_INFO("Client requesting to enter DFU mode");
+#if HAS_SCREEN
+ IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
+#endif
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
enterDfuMode();
#endif
diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp
index 9cbacc877..73ee26903 100644
--- a/src/modules/CannedMessageModule.cpp
+++ b/src/modules/CannedMessageModule.cpp
@@ -16,6 +16,7 @@
#include "graphics/draw/NotificationRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h"
+#include "input/SerialKeyboard.h"
#include "main.h" // for cardkb_found
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
#include "modules/AdminModule.h"
@@ -1017,8 +1018,7 @@ int32_t CannedMessageModule::runOnce()
// Clean up virtual keyboard if needed when going inactive
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) {
LOG_INFO("Performing delayed virtual keyboard cleanup");
- delete graphics::NotificationRenderer::virtualKeyboard;
- graphics::NotificationRenderer::virtualKeyboard = nullptr;
+ graphics::OnScreenKeyboardModule::instance().stop(false);
}
temporaryMessage = "";
@@ -1035,9 +1035,7 @@ int32_t CannedMessageModule::runOnce()
// Clean up virtual keyboard after sending
if (graphics::NotificationRenderer::virtualKeyboard) {
LOG_INFO("Cleaning up virtual keyboard after message send");
- delete graphics::NotificationRenderer::virtualKeyboard;
- graphics::NotificationRenderer::virtualKeyboard = nullptr;
- graphics::NotificationRenderer::textInputCallback = nullptr;
+ graphics::OnScreenKeyboardModule::instance().stop(false);
graphics::NotificationRenderer::resetBanner();
}
@@ -1095,9 +1093,7 @@ int32_t CannedMessageModule::runOnce()
// Clean up virtual keyboard if it exists during timeout
if (graphics::NotificationRenderer::virtualKeyboard) {
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
- delete graphics::NotificationRenderer::virtualKeyboard;
- graphics::NotificationRenderer::virtualKeyboard = nullptr;
- graphics::NotificationRenderer::textInputCallback = nullptr;
+ graphics::OnScreenKeyboardModule::instance().stop(false);
graphics::NotificationRenderer::resetBanner();
}
@@ -1848,7 +1844,88 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
}
- // --- Draw Free Text input with multi-emote support and proper line wrapping ---
+#if INPUTBROKER_SERIAL_TYPE == 1
+ // Chatter Modifier key mode label (right side)
+ {
+ uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0;
+ const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#";
+
+ display->setFont(FONT_SMALL);
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const int16_t th = FONT_HEIGHT_SMALL;
+ const int16_t tw = display->getStringWidth(label);
+ const int16_t padX = 3;
+ const int16_t padY = 2;
+ const int16_t r = 3;
+
+ const int16_t bw = tw + padX * 2;
+ const int16_t bh = th + padY * 2;
+
+ const int16_t bx = x + display->getWidth() - bw - 2;
+ const int16_t by = y + display->getHeight() - bh - 2;
+
+ display->setColor(WHITE);
+ display->fillRect(bx + r, by, bw - r * 2, bh);
+ display->fillRect(bx, by + r, r, bh - r * 2);
+ display->fillRect(bx + bw - r, by + r, r, bh - r * 2);
+ display->fillCircle(bx + r, by + r, r);
+ display->fillCircle(bx + bw - r - 1, by + r, r);
+ display->fillCircle(bx + r, by + bh - r - 1, r);
+ display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r);
+
+ display->setColor(BLACK);
+ display->drawString(bx + padX, by + padY, label);
+ }
+
+ // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + ◄”)
+ {
+ display->setFont(FONT_SMALL);
+ display->setTextAlignment(TEXT_ALIGN_LEFT);
+
+ const char *label = "Dest: Shift + ";
+ int16_t labelW = display->getStringWidth(label);
+
+ // triangle size visually matches glyph height, not full line height
+ const int triH = FONT_HEIGHT_SMALL - 3;
+ const int triW = triH * 0.7;
+
+ const int16_t padX = 3;
+ const int16_t padY = 2;
+ const int16_t r = 3;
+
+ const int16_t bw = labelW + triW + padX * 2 + 2;
+ const int16_t bh = FONT_HEIGHT_SMALL + padY * 2;
+
+ const int16_t bx = x + 2;
+ const int16_t by = y + display->getHeight() - bh - 2;
+
+ // Rounded white box
+ display->setColor(WHITE);
+ display->fillRect(bx + r, by, bw - (r * 2), bh);
+ display->fillRect(bx, by + r, r, bh - (r * 2));
+ display->fillRect(bx + bw - r, by + r, r, bh - (r * 2));
+ display->fillCircle(bx + r, by + r, r);
+ display->fillCircle(bx + bw - r - 1, by + r, r);
+ display->fillCircle(bx + r, by + bh - r - 1, r);
+ display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r);
+
+ // Draw text
+ display->setColor(BLACK);
+ display->drawString(bx + padX, by + padY, label);
+
+ // Perfectly center triangle on text baseline
+ int16_t tx = bx + padX + labelW;
+ int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering
+
+ // ◄ Left-pointing triangle
+ display->fillTriangle(tx + triW, ty, // top-right
+ tx, ty + triH / 2, // left center
+ tx + triW, ty + triH // bottom-right
+ );
+ }
+#endif
+ // Draw Free Text input with multi-emote support and proper line wrapping
display->setColor(WHITE);
{
int inputY = 0 + y + FONT_HEIGHT_SMALL;
diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp
index 91e96b8d4..6d52a3e46 100644
--- a/src/modules/ExternalNotificationModule.cpp
+++ b/src/modules/ExternalNotificationModule.cpp
@@ -168,7 +168,7 @@ int32_t ExternalNotificationModule::runOnce()
delay = EXT_NOTIFICATION_FAST_THREAD_MS;
#endif
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
drv.go();
#endif
}
@@ -283,7 +283,7 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on)
#ifdef UNPHONE
unphone.rgb(red, green, blue);
#endif
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
if (on) {
drv.go();
} else {
@@ -310,8 +310,7 @@ void ExternalNotificationModule::stopNow()
rtttl::stop();
#ifdef HAS_I2S
LOG_INFO("Stop audioThread playback");
- if (audioThread->isPlaying())
- audioThread->stop();
+ audioThread->stop();
#endif
// Turn off all outputs
LOG_INFO("Turning off setExternalStates");
@@ -320,7 +319,7 @@ void ExternalNotificationModule::stopNow()
externalTurnedOn[i] = 0;
}
setIntervalFromNow(0);
-#ifdef T_WATCH_S3
+#if defined(T_WATCH_S3) || defined(T_LORA_PAGER)
drv.stop();
#endif
@@ -542,6 +541,19 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
(!isBroadcast(mp.to) && isToUs(&mp))) {
// Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us
isNagging = true;
+#ifdef T_LORA_PAGER
+ if (canBuzz()) {
+ drv.setWaveform(0, 16); // Long buzzer 100%
+ drv.setWaveform(1, 0); // Pause
+ drv.setWaveform(2, 16);
+ drv.setWaveform(3, 0);
+ drv.setWaveform(4, 16);
+ drv.setWaveform(5, 0);
+ drv.setWaveform(6, 16);
+ drv.setWaveform(7, 0);
+ drv.go();
+ }
+#endif
if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) {
setExternalState(2, true);
} else {
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index 827524fc3..63392f7e4 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -181,25 +181,25 @@ void setupModules()
// new ReplyModule();
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
-#ifndef T_LORA_PAGER
- rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
- if (!rotaryEncoderInterruptImpl1->init()) {
- delete rotaryEncoderInterruptImpl1;
- rotaryEncoderInterruptImpl1 = nullptr;
- }
-#elif defined(T_LORA_PAGER)
+#if defined(T_LORA_PAGER)
// use a special FSM based rotary encoder version for T-LoRa Pager
rotaryEncoderImpl = new RotaryEncoderImpl();
if (!rotaryEncoderImpl->init()) {
delete rotaryEncoderImpl;
rotaryEncoderImpl = nullptr;
}
-#else
+#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
upDownInterruptImpl1 = new UpDownInterruptImpl1();
if (!upDownInterruptImpl1->init()) {
delete upDownInterruptImpl1;
upDownInterruptImpl1 = nullptr;
}
+#else
+ rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
+ if (!rotaryEncoderInterruptImpl1->init()) {
+ delete rotaryEncoderInterruptImpl1;
+ rotaryEncoderInterruptImpl1 = nullptr;
+ }
#endif
cardKbI2cImpl = new CardKbI2cImpl();
cardKbI2cImpl->init();
@@ -217,7 +217,7 @@ void setupModules()
}
#endif // HAS_BUTTON
#if ARCH_PORTDUINO
- if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+ if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
seesawRotary = new SeesawRotary("SeesawRotary");
if (!seesawRotary->init()) {
delete seesawRotary;
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index aaab019d6..7db8b66cc 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -7,17 +7,41 @@
#include "configuration.h"
#include "main.h"
#include
+#include
+
+#ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS
+#define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60)
+#endif
NodeInfoModule *nodeInfoModule;
+static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS;
+
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
{
+ suppressReplyForCurrentRequest = false;
+
if (mp.from == nodeDB->getNodeNum()) {
LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
return false;
}
auto p = *pptr;
+
+ if (mp.decoded.want_response) {
+ const NodeNum sender = getFrom(&mp);
+ const uint32_t now = mp.rx_time ? mp.rx_time : getTime();
+ auto it = lastNodeInfoSeen.find(sender);
+ if (it != lastNodeInfoSeen.end()) {
+ uint32_t sinceLast = now >= it->second ? now - it->second : 0;
+ if (sinceLast < NodeInfoReplySuppressSeconds) {
+ suppressReplyForCurrentRequest = true;
+ }
+ }
+ lastNodeInfoSeen[sender] = now;
+ pruneLastNodeInfoCache();
+ }
+
if (p.is_licensed != owner.is_licensed) {
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
return true;
@@ -42,6 +66,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
service->sendToPhone(packetCopy);
}
+ pruneLastNodeInfoCache();
+
// LOG_DEBUG("did handleReceived");
return false; // Let others look at this message also if they want
}
@@ -68,9 +94,11 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
if (p) { // Check whether we didn't ignore it
p->to = dest;
- p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
+ bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
wantReplies;
+
+ p->decoded.want_response = requestWantResponse;
if (_shorterTimeout)
p->priority = meshtastic_MeshPacket_Priority_DEFAULT;
else
@@ -89,6 +117,13 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
meshtastic_MeshPacket *NodeInfoModule::allocReply()
{
+ if (suppressReplyForCurrentRequest) {
+ LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago");
+ ignoreRequest = true;
+ suppressReplyForCurrentRequest = false;
+ return NULL;
+ }
+
if (!airTime->isTxAllowedChannelUtil(false)) {
ignoreRequest = true; // Mark it as ignored for MeshModule
LOG_DEBUG("Skip send NodeInfo > 40%% ch. util");
@@ -125,6 +160,29 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
}
}
+void NodeInfoModule::pruneLastNodeInfoCache()
+{
+ if (!nodeDB || !nodeDB->meshNodes)
+ return;
+
+ const size_t maxEntries = nodeDB->meshNodes->size();
+
+ for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) {
+ if (!nodeDB->getMeshNode(it->first)) {
+ it = lastNodeInfoSeen.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) {
+ auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(),
+ [](const std::pair &lhs,
+ const std::pair &rhs) { return lhs.second < rhs.second; });
+ lastNodeInfoSeen.erase(oldestIt);
+ }
+}
+
NodeInfoModule::NodeInfoModule()
: ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo")
{
diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h
index 572b81700..d16fbeac2 100644
--- a/src/modules/NodeInfoModule.h
+++ b/src/modules/NodeInfoModule.h
@@ -1,5 +1,6 @@
#pragma once
#include "ProtobufModule.h"
+#include