mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 14:52:32 +00:00
Compare commits
273 Commits
revert-2m-
...
multi-mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634aa9857f | ||
|
|
eb0cdc44ba | ||
|
|
ddc8d2424b | ||
|
|
0597ae42c9 | ||
|
|
001699f16d | ||
|
|
f09183f5f9 | ||
|
|
59e25ff1c2 | ||
|
|
11cd6370d4 | ||
|
|
bca169ddaf | ||
|
|
b150c56735 | ||
|
|
d6bf0a330a | ||
|
|
f3ebdec49e | ||
|
|
f68ef9f7d7 | ||
|
|
68250dc937 | ||
|
|
2dc54cdd4b | ||
|
|
1b7104b1e2 | ||
|
|
ef99939d6f | ||
|
|
4abd3f9a1f | ||
|
|
cfea55d77d | ||
|
|
bcfe069997 | ||
|
|
71bc99938c | ||
|
|
eeb5d0478e | ||
|
|
4e7b87b099 | ||
|
|
4fc96bdf83 | ||
|
|
2634978e57 | ||
|
|
7515123307 | ||
|
|
467c042bf7 | ||
|
|
bbfddea3af | ||
|
|
cc4c41167c | ||
|
|
fff2bbf4a0 | ||
|
|
fba92229a6 | ||
|
|
4a35fd8317 | ||
|
|
ff0a4ea320 | ||
|
|
83b603827c | ||
|
|
e254f39925 | ||
|
|
0b22382cbb | ||
|
|
2032ff1c32 | ||
|
|
5910cc2e26 | ||
|
|
ee80ec7b68 | ||
|
|
9d5b69f9b4 | ||
|
|
aa72e397f2 | ||
|
|
ec0dfb7337 | ||
|
|
c55bea8460 | ||
|
|
aa605fc4a2 | ||
|
|
d75680a2dd | ||
|
|
817f3b9ec8 | ||
|
|
d4b44b1c63 | ||
|
|
decd58cd5c | ||
|
|
e691bd9732 | ||
|
|
5ae962731b | ||
|
|
6bad81f8dd | ||
|
|
69b9977fc1 | ||
|
|
20887eb1f6 | ||
|
|
0726bb4b56 | ||
|
|
6b11991be0 | ||
|
|
8e63dcf59a | ||
|
|
042543eb25 | ||
|
|
9d7bc381a1 | ||
|
|
6c02f82d87 | ||
|
|
ae8d3fbb3d | ||
|
|
928739e0fb | ||
|
|
8be7915fc7 | ||
|
|
c052963395 | ||
|
|
65c418d4e1 | ||
|
|
c3a69a2742 | ||
|
|
66ff1536f3 | ||
|
|
2bcf342d3d | ||
|
|
5671e9d96f | ||
|
|
bd4bcb94f0 | ||
|
|
85c2226c63 | ||
|
|
6df2ae9f4a | ||
|
|
12d1f50015 | ||
|
|
4b2f241478 | ||
|
|
eb087849c0 | ||
|
|
b86a4c0b43 | ||
|
|
94aedff6ae | ||
|
|
f4fd3e7812 | ||
|
|
fb932461c1 | ||
|
|
03dba769dd | ||
|
|
eeaafda62a | ||
|
|
87c0090e98 | ||
|
|
c0728b0eba | ||
|
|
1381bb1dfd | ||
|
|
903ddb5b95 | ||
|
|
6e9fd189b4 | ||
|
|
3f40916223 | ||
|
|
34b1652f38 | ||
|
|
b29fe39f59 | ||
|
|
1b4925bd07 | ||
|
|
0828c445fb | ||
|
|
0b43c5b52d | ||
|
|
90584359e4 | ||
|
|
8a43741589 | ||
|
|
493eb32095 | ||
|
|
ab044414a7 | ||
|
|
bde661260b | ||
|
|
0d8c549269 | ||
|
|
0295685be7 | ||
|
|
7d9f629df3 | ||
|
|
ce6278f351 | ||
|
|
4bf543bf85 | ||
|
|
ebf8446c94 | ||
|
|
a3cf1fb468 | ||
|
|
ed0fadcdd2 | ||
|
|
a2600723d5 | ||
|
|
7236e3647b | ||
|
|
6ae6b3663c | ||
|
|
f0af449dc2 | ||
|
|
43cfe0336b | ||
|
|
ceca3d131e | ||
|
|
69363d3bf4 | ||
|
|
0607476e76 | ||
|
|
922d15620f | ||
|
|
1fcbee34f4 | ||
|
|
da7d2815fc | ||
|
|
586d4d2b99 | ||
|
|
f6cf92ca84 | ||
|
|
3a396e551d | ||
|
|
b306bf042c | ||
|
|
c6f3b2d76a | ||
|
|
1ecfbe276d | ||
|
|
8929231290 | ||
|
|
aefebbbfc8 | ||
|
|
bfcfcb6805 | ||
|
|
cde626cf56 | ||
|
|
fb2223ada3 | ||
|
|
46391ff5e3 | ||
|
|
9441f0c143 | ||
|
|
b3f4e81827 | ||
|
|
c30eb4b659 | ||
|
|
5c600ded1c | ||
|
|
dd4b5d443a | ||
|
|
09a38139f5 | ||
|
|
efc304ccf6 | ||
|
|
65a5cec1c1 | ||
|
|
aace45305d | ||
|
|
9e9eff0a43 | ||
|
|
6c3d2fd7b9 | ||
|
|
e2d44829fe | ||
|
|
9f6b5f0a17 | ||
|
|
ccdb27d377 | ||
|
|
ef5016aa12 | ||
|
|
33cbf35251 | ||
|
|
6647900196 | ||
|
|
c3b4e6bc9e | ||
|
|
4c176d7829 | ||
|
|
541a6b55ba | ||
|
|
5b10510577 | ||
|
|
01a7389241 | ||
|
|
022825e53b | ||
|
|
db62d92cf6 | ||
|
|
263ae8c79f | ||
|
|
9ffdaedb48 | ||
|
|
6926a50d70 | ||
|
|
ec04aa0a89 | ||
|
|
c9aeafd227 | ||
|
|
2c05baa1b4 | ||
|
|
3cb4e0e195 | ||
|
|
d25110f971 | ||
|
|
9a12304769 | ||
|
|
bc9c1877d8 | ||
|
|
dd95748966 | ||
|
|
a635ee3de2 | ||
|
|
69f489ee70 | ||
|
|
b26c95d4b9 | ||
|
|
f09ac16f19 | ||
|
|
c0361d2aea | ||
|
|
13458d3a6a | ||
|
|
14dfce5e03 | ||
|
|
f012f98024 | ||
|
|
fbf7ab0455 | ||
|
|
4f79475b3c | ||
|
|
4aad9083bb | ||
|
|
04c7720f4b | ||
|
|
3e3f03c78d | ||
|
|
caff68f2f3 | ||
|
|
71353874a1 | ||
|
|
c8f3cbb0f9 | ||
|
|
62eaabc940 | ||
|
|
67c24c08cc | ||
|
|
4bd53500c6 | ||
|
|
6cd64cc228 | ||
|
|
a05936f655 | ||
|
|
aaf4a7e59e | ||
|
|
c180f23026 | ||
|
|
ee3c7f2272 | ||
|
|
2f65721774 | ||
|
|
849a749b81 | ||
|
|
214aa8b59d | ||
|
|
8fb825e0e0 | ||
|
|
4ca56ec9cb | ||
|
|
3bb5a3341c | ||
|
|
7aa5b93895 | ||
|
|
835f13031c | ||
|
|
5f8f3cf8be | ||
|
|
56656a4e6a | ||
|
|
2f53f3f1dc | ||
|
|
8611175628 | ||
|
|
b0c0faa075 | ||
|
|
f35f72edb1 | ||
|
|
0b11f93880 | ||
|
|
e38925834d | ||
|
|
551086324b | ||
|
|
50a65a1393 | ||
|
|
65dcd8254e | ||
|
|
f9e31558d1 | ||
|
|
6dfaf23baf | ||
|
|
544331d367 | ||
|
|
5e5a449226 | ||
|
|
9e9d2af7c8 | ||
|
|
e934f8f0b3 | ||
|
|
1d7fe20520 | ||
|
|
abeeb12d96 | ||
|
|
97578fb9c0 | ||
|
|
e606d88297 | ||
|
|
163d8e0540 | ||
|
|
784e71f2fa | ||
|
|
84cef67b82 | ||
|
|
6d899c9fd9 | ||
|
|
6069dc2ad8 | ||
|
|
e6093533ab | ||
|
|
334089aa97 | ||
|
|
e93b65706b | ||
|
|
7970a32074 | ||
|
|
20e9703e1b | ||
|
|
541e050e40 | ||
|
|
974b3e6133 | ||
|
|
9e520d008b | ||
|
|
def5018c9d | ||
|
|
d5ce4696f3 | ||
|
|
cb8a8a2c52 | ||
|
|
993e874eec | ||
|
|
9f62007d94 | ||
|
|
67c0000f87 | ||
|
|
e7cdc7cc29 | ||
|
|
b549786bbb | ||
|
|
103f73e7c9 | ||
|
|
383d95ade1 | ||
|
|
9b57b21ab4 | ||
|
|
0b96486e7e | ||
|
|
b8d33a3280 | ||
|
|
c9314c78ca | ||
|
|
bbec5177ba | ||
|
|
8860f6195f | ||
|
|
bebb1e9e8d | ||
|
|
13eb53fcf6 | ||
|
|
4e840943c6 | ||
|
|
65bb78b34f | ||
|
|
0476eeec9e | ||
|
|
7642d0c401 | ||
|
|
526c7f8e4d | ||
|
|
7f1dc4e76a | ||
|
|
34bb858588 | ||
|
|
4ae5e8a48e | ||
|
|
e3553c4eb3 | ||
|
|
3e4f654f58 | ||
|
|
28502c93c9 | ||
|
|
07d3726cde | ||
|
|
ebbb8a6f9f | ||
|
|
dd7a5cf31f | ||
|
|
46a8a9a89e | ||
|
|
4d10026185 | ||
|
|
91e1029a3b | ||
|
|
8333ceb55e | ||
|
|
ea7638b4ec | ||
|
|
3780290581 | ||
|
|
6ead5c04bd | ||
|
|
4e61016a44 | ||
|
|
8040bb20b4 | ||
|
|
3d8b4a68b8 | ||
|
|
abcc166f3a | ||
|
|
d779821f0e | ||
|
|
cf9bc7ac00 |
@@ -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.
|
||||
|
||||
1
.github/actionlint.yaml
vendored
1
.github/actionlint.yaml
vendored
@@ -2,4 +2,5 @@
|
||||
self-hosted-runner:
|
||||
# Labels of self-hosted runner in array of strings.
|
||||
labels:
|
||||
- arctastic
|
||||
- test-runner
|
||||
|
||||
2
.github/actions/build-variant/action.yml
vendored
2
.github/actions/build-variant/action.yml
vendored
@@ -102,7 +102,7 @@ runs:
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
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 }}
|
||||
|
||||
21
.github/workflows/build_firmware.yml
vendored
21
.github/workflows/build_firmware.yml
vendored
@@ -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,29 @@ jobs:
|
||||
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
|
||||
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||
|
||||
- name: Echo manifest from release/firmware-*.mt.json to job summary
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
PIO_ENV: ${{ inputs.pio_env }}
|
||||
run: |
|
||||
echo "## Manifest: \`$PIO_ENV\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
|
||||
6
.github/workflows/build_one_target.yml
vendored
6
.github/workflows/build_one_target.yml
vendored
@@ -119,7 +119,7 @@ jobs:
|
||||
./firmware-*.bin
|
||||
./firmware-*.uf2
|
||||
./firmware-*.hex
|
||||
./firmware-*-ota.zip
|
||||
./firmware-*.zip
|
||||
./device-*.sh
|
||||
./device-*.bat
|
||||
./littlefs-*.bin
|
||||
@@ -139,8 +139,8 @@ 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
|
||||
|
||||
67
.github/workflows/main_matrix.yml
vendored
67
.github/workflows/main_matrix.yml
vendored
@@ -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]
|
||||
@@ -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
|
||||
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
|
||||
@@ -209,8 +212,8 @@ 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
|
||||
@@ -218,7 +221,7 @@ jobs:
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
@@ -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@v5
|
||||
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:
|
||||
@@ -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
|
||||
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@v6
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./publish
|
||||
|
||||
- name: Get manifest artifact
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: manifest-${{ needs.version.outputs.long }}
|
||||
path: ./publish
|
||||
|
||||
- name: Publish firmware to meshtastic.github.io
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
|
||||
19
.github/workflows/merge_queue.yml
vendored
19
.github/workflows/merge_queue.yml
vendored
@@ -168,7 +168,7 @@ jobs:
|
||||
./firmware-*.bin
|
||||
./firmware-*.uf2
|
||||
./firmware-*.hex
|
||||
./firmware-*-ota.zip
|
||||
./firmware-*.zip
|
||||
./device-*.sh
|
||||
./device-*.bat
|
||||
./littlefs-*.bin
|
||||
@@ -188,8 +188,8 @@ 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
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
|
||||
|
||||
2
.github/workflows/pr_enforce_labels.yml
vendored
2
.github/workflows/pr_enforce_labels.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const labels = context.payload.pull_request.labels.map(label => label.name);
|
||||
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk'];
|
||||
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup'];
|
||||
const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
|
||||
if (!hasRequiredLabel) {
|
||||
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
|
||||
|
||||
2
.github/workflows/pr_tests.yml
vendored
2
.github/workflows/pr_tests.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
if: needs.native-tests.result != 'skipped'
|
||||
uses: actions/download-artifact@v6
|
||||
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
|
||||
|
||||
2
.github/workflows/release_channels.yml
vendored
2
.github/workflows/release_channels.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/stale_bot.yml
vendored
2
.github/workflows/stale_bot.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v10.1.0
|
||||
uses: actions/stale@v10.1.1
|
||||
with:
|
||||
days-before-stale: 45
|
||||
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
|
||||
|
||||
14
.github/workflows/test_native.yml
vendored
14
.github/workflows/test_native.yml
vendored
@@ -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..."
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
if: always() # run this step even if previous step failed
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: ./testreport.xml
|
||||
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v5
|
||||
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
|
||||
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: Test Report
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
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
|
||||
|
||||
@@ -165,5 +165,5 @@ jobs:
|
||||
- name: Save Code Coverage Report
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: code-coverage-report-${{ steps.version.outputs.long }}.zip
|
||||
name: code-coverage-report-${{ steps.version.outputs.long }}
|
||||
path: code-coverage-report
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
2
.github/workflows/update_protobufs.yml
vendored
2
.github/workflows/update_protobufs.yml
vendored
@@ -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
|
||||
|
||||
@@ -9,24 +9,24 @@ plugins:
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.495
|
||||
- renovate@42.27.1
|
||||
- prettier@3.7.3
|
||||
- trufflehog@3.91.1
|
||||
- renovate@42.42.2
|
||||
- prettier@3.7.4
|
||||
- trufflehog@3.92.1
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.9.2
|
||||
- trivy@0.67.2
|
||||
- trivy@0.68.1
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.7
|
||||
- ruff@0.14.8
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.46.0
|
||||
- oxipng@9.1.5
|
||||
- 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
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \
|
||||
|
||||
# ##### PRODUCTION BUILD #############
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
LABEL org.opencontainers.image.title="Meshtastic" \
|
||||
org.opencontainers.image.description="Alpine Meshtastic daemon" \
|
||||
org.opencontainers.image.url="https://meshtastic.org" \
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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,37 @@ 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
|
||||
|
||||
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/$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
|
||||
echo "Copying .hex file"
|
||||
cp $SRCHEX $OUTDIR/
|
||||
fi
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<exc>[0-9]*)\\):$")
|
||||
COUNTER_REGEX = re.compile(
|
||||
@@ -89,7 +89,7 @@ POINTER_REGEX = re.compile(
|
||||
STACK_BEGIN = ">>>stack>>>"
|
||||
STACK_END = "<<<stack<<<"
|
||||
STACK_REGEX = re.compile(
|
||||
"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$"
|
||||
r"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[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":
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
set -e
|
||||
pio run --environment native
|
||||
.pio/build/native/program "$@"
|
||||
.pio/build/native/meshtasticd "$@"
|
||||
|
||||
@@ -2,98 +2,77 @@
|
||||
# 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 = []
|
||||
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():
|
||||
d = {
|
||||
"name": p,
|
||||
"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,7 +118,7 @@ flags = [
|
||||
"-DBUILD_EPOCH=" + str(build_epoch),
|
||||
] + pref_flags
|
||||
|
||||
print ("Using flags:")
|
||||
print("Using flags:")
|
||||
for flag in flags:
|
||||
print(flag)
|
||||
|
||||
@@ -180,4 +159,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,
|
||||
)
|
||||
|
||||
19
bin/platformio-pre.py
Normal file
19
bin/platformio-pre.py
Normal file
@@ -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')}")
|
||||
@@ -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..."
|
||||
|
||||
1
debian/rules
vendored
1
debian/rules
vendored
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
86
extra_scripts/esp32_extra.py
Executable file
86
extra_scripts/esp32_extra.py
Executable file
@@ -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"])
|
||||
73
extra_scripts/esp32_pre.py
Executable file
73
extra_scripts/esp32_pre.py
Executable file
@@ -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)
|
||||
50
extra_scripts/nrf52_extra.py
Executable file
50
extra_scripts/nrf52_extra.py
Executable file
@@ -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)
|
||||
@@ -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",
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
@@ -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/3bf332240416c5cb8c919fac2a0ec7260eb3be75.zip
|
||||
https://github.com/meshtastic/device-ui/archive/4fb5f24787caa841b58dbf623a52c4c5861d6722.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
|
||||
|
||||
Submodule protobufs updated: 52fa252f1e...4095e59890
426
src/MessageStore.cpp
Normal file
426
src/MessageStore.cpp
Normal file
@@ -0,0 +1,426 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "FSCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
|
||||
#endif
|
||||
|
||||
// Global message text pool and state
|
||||
static char *g_messagePool = nullptr;
|
||||
static size_t g_poolWritePos = 0;
|
||||
|
||||
// Reset pool (called on boot or clear)
|
||||
static inline void resetMessagePool()
|
||||
{
|
||||
if (!g_messagePool) {
|
||||
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
|
||||
if (!g_messagePool) {
|
||||
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_poolWritePos = 0;
|
||||
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
|
||||
}
|
||||
|
||||
// Allocate text in pool and return offset
|
||||
// If not enough space remains, wrap around (ring buffer style)
|
||||
static inline uint16_t storeTextInPool(const char *src, size_t len)
|
||||
{
|
||||
if (len >= MAX_MESSAGE_SIZE)
|
||||
len = MAX_MESSAGE_SIZE - 1;
|
||||
|
||||
// Wrap pool if out of space
|
||||
if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
|
||||
g_poolWritePos = 0;
|
||||
}
|
||||
|
||||
uint16_t offset = g_poolWritePos;
|
||||
memcpy(&g_messagePool[g_poolWritePos], src, len);
|
||||
g_messagePool[g_poolWritePos + len] = '\0';
|
||||
g_poolWritePos += (len + 1);
|
||||
return offset;
|
||||
}
|
||||
|
||||
// Retrieve a const pointer to message text by offset
|
||||
static inline const char *getTextFromPool(uint16_t offset)
|
||||
{
|
||||
if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
|
||||
return "";
|
||||
return &g_messagePool[offset];
|
||||
}
|
||||
|
||||
// Helper: assign a timestamp (RTC if available, else boot-relative)
|
||||
static inline void assignTimestamp(StoredMessage &sm)
|
||||
{
|
||||
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
if (nowSecs) {
|
||||
sm.timestamp = nowSecs;
|
||||
sm.isBootRelative = false;
|
||||
} else {
|
||||
sm.timestamp = millis() / 1000;
|
||||
sm.isBootRelative = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic push with cap (used by live + persisted queues)
|
||||
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
|
||||
{
|
||||
if (queue.size() >= MAX_MESSAGES_SAVED)
|
||||
queue.pop_front();
|
||||
queue.push_back(msg);
|
||||
}
|
||||
|
||||
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
|
||||
{
|
||||
if (queue.size() >= MAX_MESSAGES_SAVED)
|
||||
queue.pop_front();
|
||||
queue.emplace_back(std::move(msg));
|
||||
}
|
||||
|
||||
MessageStore::MessageStore(const std::string &label)
|
||||
{
|
||||
filename = "/Messages_" + label + ".msgs";
|
||||
resetMessagePool(); // initialize text pool on boot
|
||||
}
|
||||
|
||||
// Live message handling (RAM only)
|
||||
void MessageStore::addLiveMessage(StoredMessage &&msg)
|
||||
{
|
||||
pushWithLimit(liveMessages, std::move(msg));
|
||||
}
|
||||
void MessageStore::addLiveMessage(const StoredMessage &msg)
|
||||
{
|
||||
pushWithLimit(liveMessages, msg);
|
||||
}
|
||||
|
||||
// Add from incoming/outgoing packet
|
||||
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
|
||||
{
|
||||
StoredMessage sm;
|
||||
assignTimestamp(sm);
|
||||
sm.channelIndex = packet.channel;
|
||||
|
||||
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
|
||||
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
|
||||
sm.textOffset = storeTextInPool(payload, len);
|
||||
sm.textLength = len;
|
||||
|
||||
// Determine sender
|
||||
uint32_t localNode = nodeDB->getNodeNum();
|
||||
sm.sender = (packet.from == 0) ? localNode : packet.from;
|
||||
|
||||
sm.dest = packet.to;
|
||||
|
||||
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
|
||||
|
||||
if (packet.from == 0) {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::NONE;
|
||||
} else {
|
||||
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
|
||||
sm.ackStatus = AckStatus::ACKED;
|
||||
}
|
||||
|
||||
addLiveMessage(sm);
|
||||
return liveMessages.back();
|
||||
}
|
||||
|
||||
// Outgoing/manual message
|
||||
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
|
||||
{
|
||||
StoredMessage sm;
|
||||
|
||||
// Always use our local time (helper handles RTC vs boot time)
|
||||
assignTimestamp(sm);
|
||||
|
||||
sm.sender = sender;
|
||||
sm.channelIndex = channelIndex;
|
||||
sm.textOffset = storeTextInPool(text.c_str(), text.size());
|
||||
sm.textLength = text.size();
|
||||
|
||||
// Use the provided destination
|
||||
sm.dest = sender;
|
||||
sm.type = MessageType::DM_TO_US;
|
||||
|
||||
// Outgoing messages always start with unknown ack status
|
||||
sm.ackStatus = AckStatus::NONE;
|
||||
|
||||
addLiveMessage(sm);
|
||||
}
|
||||
|
||||
#if ENABLE_MESSAGE_PERSISTENCE
|
||||
|
||||
// Compact, fixed-size on-flash representation using offset + length
|
||||
struct __attribute__((packed)) StoredMessageRecord {
|
||||
uint32_t timestamp;
|
||||
uint32_t sender;
|
||||
uint8_t channelIndex;
|
||||
uint32_t dest;
|
||||
uint8_t isBootRelative;
|
||||
uint8_t ackStatus; // static_cast<uint8_t>(AckStatus)
|
||||
uint8_t type; // static_cast<uint8_t>(MessageType)
|
||||
uint16_t textLength; // message length
|
||||
char text[MAX_MESSAGE_SIZE]; // store actual text here
|
||||
};
|
||||
|
||||
// Serialize one StoredMessage to flash
|
||||
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
|
||||
{
|
||||
StoredMessageRecord rec = {};
|
||||
rec.timestamp = m.timestamp;
|
||||
rec.sender = m.sender;
|
||||
rec.channelIndex = m.channelIndex;
|
||||
rec.dest = m.dest;
|
||||
rec.isBootRelative = m.isBootRelative;
|
||||
rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
|
||||
rec.type = static_cast<uint8_t>(m.type);
|
||||
rec.textLength = m.textLength;
|
||||
|
||||
// Copy the actual text into the record from RAM pool
|
||||
const char *txt = getTextFromPool(m.textOffset);
|
||||
strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
|
||||
rec.text[MAX_MESSAGE_SIZE - 1] = '\0';
|
||||
|
||||
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
|
||||
}
|
||||
|
||||
// Deserialize one StoredMessage from flash; returns false on short read
|
||||
static inline bool readMessageRecord(File &f, StoredMessage &m)
|
||||
{
|
||||
StoredMessageRecord rec = {};
|
||||
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
|
||||
return false;
|
||||
|
||||
m.timestamp = rec.timestamp;
|
||||
m.sender = rec.sender;
|
||||
m.channelIndex = rec.channelIndex;
|
||||
m.dest = rec.dest;
|
||||
m.isBootRelative = rec.isBootRelative;
|
||||
m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
|
||||
m.type = static_cast<MessageType>(rec.type);
|
||||
m.textLength = rec.textLength;
|
||||
|
||||
// 💡 Re-store text into pool and update offset
|
||||
m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
|
||||
m.textOffset = storeTextInPool(rec.text, m.textLength);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MessageStore::saveToFlash()
|
||||
{
|
||||
#ifdef FSCom
|
||||
// Ensure root exists
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/");
|
||||
spiLock->unlock();
|
||||
|
||||
SafeFile f(filename.c_str(), false);
|
||||
|
||||
spiLock->lock();
|
||||
uint8_t count = static_cast<uint8_t>(liveMessages.size());
|
||||
if (count > MAX_MESSAGES_SAVED)
|
||||
count = MAX_MESSAGES_SAVED;
|
||||
f.write(&count, 1);
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
writeMessageRecord(f, liveMessages[i]);
|
||||
}
|
||||
spiLock->unlock();
|
||||
|
||||
f.close();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MessageStore::loadFromFlash()
|
||||
{
|
||||
std::deque<StoredMessage>().swap(liveMessages);
|
||||
resetMessagePool(); // reset pool when loading
|
||||
|
||||
#ifdef FSCom
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
if (!FSCom.exists(filename.c_str()))
|
||||
return;
|
||||
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
if (!f)
|
||||
return;
|
||||
|
||||
uint8_t count = 0;
|
||||
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
||||
if (count > MAX_MESSAGES_SAVED)
|
||||
count = MAX_MESSAGES_SAVED;
|
||||
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
StoredMessage m;
|
||||
if (!readMessageRecord(f, m))
|
||||
break;
|
||||
liveMessages.push_back(m);
|
||||
}
|
||||
|
||||
f.close();
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
// If persistence is disabled, these functions become no-ops
|
||||
void MessageStore::saveToFlash() {}
|
||||
void MessageStore::loadFromFlash() {}
|
||||
#endif
|
||||
|
||||
// Clear all messages (RAM + persisted queue)
|
||||
void MessageStore::clearAllMessages()
|
||||
{
|
||||
std::deque<StoredMessage>().swap(liveMessages);
|
||||
resetMessagePool();
|
||||
|
||||
#ifdef FSCom
|
||||
SafeFile f(filename.c_str(), false);
|
||||
uint8_t count = 0;
|
||||
f.write(&count, 1); // write "0 messages"
|
||||
f.close();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Internal helper: erase first or last message matching a predicate
|
||||
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
|
||||
{
|
||||
if (fromBack) {
|
||||
// Iterate from the back and erase all matches from the end
|
||||
for (auto it = deque.rbegin(); it != deque.rend();) {
|
||||
if (pred(*it)) {
|
||||
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Manual forward search to erase all matches
|
||||
for (auto it = deque.begin(); it != deque.end();) {
|
||||
if (pred(*it)) {
|
||||
it = deque.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete oldest message (RAM + persisted queue)
|
||||
void MessageStore::deleteOldestMessage()
|
||||
{
|
||||
eraseIf(liveMessages, [](StoredMessage &) { return true; });
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
// Delete oldest message in a specific channel
|
||||
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
|
||||
{
|
||||
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
|
||||
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
|
||||
{
|
||||
uint32_t local = nodeDB->getNodeNum();
|
||||
auto pred = [&](const StoredMessage &m) {
|
||||
if (m.type != MessageType::DM_TO_US)
|
||||
return false;
|
||||
uint32_t other = (m.sender == local) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred, false);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
// Delete oldest message in a direct chat with a node
|
||||
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
|
||||
{
|
||||
auto pred = [peer](const StoredMessage &m) {
|
||||
if (m.type != MessageType::DM_TO_US)
|
||||
return false;
|
||||
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
return other == peer;
|
||||
};
|
||||
eraseIf(liveMessages, pred);
|
||||
saveToFlash();
|
||||
}
|
||||
|
||||
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::deque<StoredMessage> MessageStore::getDirectMessages() const
|
||||
{
|
||||
std::deque<StoredMessage> result;
|
||||
for (const auto &m : liveMessages) {
|
||||
if (m.type == MessageType::DM_TO_US) {
|
||||
result.push_back(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Upgrade boot-relative timestamps once RTC is valid
|
||||
// Only same-boot boot-relative messages are healed.
|
||||
// Persisted boot-relative messages from old boots stay ??? forever.
|
||||
void MessageStore::upgradeBootRelativeTimestamps()
|
||||
{
|
||||
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
if (nowSecs == 0)
|
||||
return; // Still no valid RTC
|
||||
|
||||
uint32_t bootNow = millis() / 1000;
|
||||
|
||||
auto fix = [&](std::deque<StoredMessage> &dq) {
|
||||
for (auto &m : dq) {
|
||||
if (m.isBootRelative && m.timestamp <= bootNow) {
|
||||
uint32_t bootOffset = nowSecs - bootNow;
|
||||
m.timestamp += bootOffset;
|
||||
m.isBootRelative = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
fix(liveMessages);
|
||||
}
|
||||
|
||||
const char *MessageStore::getText(const StoredMessage &msg)
|
||||
{
|
||||
// Wrapper around the internal helper
|
||||
return getTextFromPool(msg.textOffset);
|
||||
}
|
||||
|
||||
uint16_t MessageStore::storeText(const char *src, size_t len)
|
||||
{
|
||||
// Wrapper around the internal helper
|
||||
return storeTextInPool(src, len);
|
||||
}
|
||||
|
||||
// Global definition
|
||||
MessageStore messageStore("default");
|
||||
#endif
|
||||
131
src/MessageStore.h
Normal file
131
src/MessageStore.h
Normal file
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_SCREEN
|
||||
|
||||
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
|
||||
#if defined(HELTEC_MESH_SOLAR)
|
||||
#define LOG_DEBUG(...)
|
||||
#endif
|
||||
|
||||
// Enable or disable message persistence (flash storage)
|
||||
// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely
|
||||
#ifndef ENABLE_MESSAGE_PERSISTENCE
|
||||
#define ENABLE_MESSAGE_PERSISTENCE 1
|
||||
#endif
|
||||
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
// How many messages are stored (RAM + flash).
|
||||
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
|
||||
#ifndef MESSAGE_HISTORY_LIMIT
|
||||
#define MESSAGE_HISTORY_LIMIT 20
|
||||
#endif
|
||||
|
||||
// Internal alias used everywhere in code – do NOT redefine elsewhere.
|
||||
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
|
||||
|
||||
// Maximum text payload size per message in bytes.
|
||||
// This still defines the max message length, but we no longer reserve this space per message.
|
||||
#define MAX_MESSAGE_SIZE 220
|
||||
|
||||
// Total shared text pool size for all messages combined.
|
||||
// The text pool is RAM-only. Text is re-stored from flash into the pool on boot.
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
|
||||
#endif
|
||||
|
||||
// Explicit message classification
|
||||
enum class MessageType : uint8_t {
|
||||
BROADCAST = 0, // broadcast message
|
||||
DM_TO_US = 1 // direct message addressed to this node
|
||||
};
|
||||
|
||||
// Delivery status for messages we sent
|
||||
enum class AckStatus : uint8_t {
|
||||
NONE = 0, // just sent, waiting (no symbol shown)
|
||||
ACKED = 1, // got a valid ACK from destination
|
||||
NACKED = 2, // explicitly failed
|
||||
TIMEOUT = 3, // no ACK after retry window
|
||||
RELAYED = 4 // got an ACK from relay, not destination
|
||||
};
|
||||
|
||||
struct StoredMessage {
|
||||
uint32_t timestamp; // When message was created (secs since boot or RTC)
|
||||
uint32_t sender; // NodeNum of sender
|
||||
uint8_t channelIndex; // Channel index used
|
||||
uint32_t dest; // Destination node (broadcast or direct)
|
||||
MessageType type; // Derived from dest (explicit classification)
|
||||
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
|
||||
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
|
||||
|
||||
// Text storage metadata — rebuilt from flash at boot
|
||||
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
|
||||
uint16_t textLength; // Length of text in bytes
|
||||
|
||||
// Default constructor initializes all fields safely
|
||||
StoredMessage()
|
||||
: timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false),
|
||||
ackStatus(AckStatus::NONE), textOffset(0), textLength(0)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class MessageStore
|
||||
{
|
||||
public:
|
||||
explicit MessageStore(const std::string &label);
|
||||
|
||||
// Live RAM methods (always current, used by UI and runtime)
|
||||
void addLiveMessage(StoredMessage &&msg);
|
||||
void addLiveMessage(const StoredMessage &msg); // convenience overload
|
||||
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
|
||||
|
||||
// Add new messages from packets or manual input
|
||||
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
|
||||
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
|
||||
|
||||
// Persistence methods (used only on boot/shutdown)
|
||||
void saveToFlash(); // Save messages to flash
|
||||
void loadFromFlash(); // Load messages from flash
|
||||
|
||||
// Clear all messages (RAM + persisted queue + text pool)
|
||||
void clearAllMessages();
|
||||
|
||||
// Delete helpers
|
||||
void deleteOldestMessage(); // remove oldest from RAM (and flash on save)
|
||||
void deleteOldestMessageInChannel(uint8_t channel);
|
||||
void deleteOldestMessageWithPeer(uint32_t peer);
|
||||
void deleteAllMessagesInChannel(uint8_t channel);
|
||||
void deleteAllMessagesWithPeer(uint32_t peer);
|
||||
|
||||
// Unified accessor (for UI code, defaults to RAM buffer)
|
||||
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
|
||||
|
||||
// Helper filters for future use
|
||||
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
|
||||
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
|
||||
|
||||
// Upgrade boot-relative timestamps once RTC is valid
|
||||
void upgradeBootRelativeTimestamps();
|
||||
|
||||
// Retrieve the C-string text for a stored message
|
||||
static const char *getText(const StoredMessage &msg);
|
||||
|
||||
// Allocate text into pool (used by sender-side code)
|
||||
static uint16_t storeText(const char *src, size_t len);
|
||||
|
||||
// Used when loading from flash to rebuild the text pool
|
||||
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
|
||||
|
||||
private:
|
||||
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
|
||||
std::string filename; // Flash filename for persistence
|
||||
};
|
||||
|
||||
// Global instance (defined in MessageStore.cpp)
|
||||
extern MessageStore messageStore;
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,7 @@
|
||||
* For more information, see: https://meshtastic.org/
|
||||
*/
|
||||
#include "power.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "Throttle.h"
|
||||
@@ -786,7 +787,9 @@ void Power::shutdown()
|
||||
playShutdownMelody();
|
||||
#endif
|
||||
nodeDB->saveToDisk();
|
||||
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||
#ifdef PIN_LED1
|
||||
ledOff(PIN_LED1);
|
||||
|
||||
@@ -131,6 +131,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
::printf("%s ", logLevel);
|
||||
if (color) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#include "FSCommon.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "error.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -64,10 +65,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
|
||||
using graphics::Emote;
|
||||
using graphics::emotes;
|
||||
using graphics::numEmotes;
|
||||
extern MessageStore messageStore;
|
||||
|
||||
#if USE_TFTDISPLAY
|
||||
extern uint16_t TFT_MESH;
|
||||
@@ -119,10 +117,6 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
||||
// we'll need to hold onto pointers for the modules that can draw a frame.
|
||||
std::vector<MeshModule *> moduleFrames;
|
||||
|
||||
// Global variables for screen function overlay symbols
|
||||
std::vector<std::string> functionSymbol;
|
||||
std::string functionSymbolString;
|
||||
|
||||
#if HAS_GPS
|
||||
// GeoCoord object for the screen
|
||||
GeoCoord geoCoord;
|
||||
@@ -230,24 +224,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);
|
||||
@@ -278,19 +257,11 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
} else {
|
||||
// otherwise, just display the module frame that's aligned with the current frame
|
||||
module_frame = state->currentFrame;
|
||||
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
|
||||
}
|
||||
// LOG_DEBUG("Draw Module Frame %d", module_frame);
|
||||
MeshModule &pi = *moduleFrames.at(module_frame);
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
|
||||
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
return packet->from != 0 && !moduleConfig.store_forward.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -337,16 +308,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
{
|
||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||
|
||||
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
|
||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
|
||||
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
|
||||
uint8_t TFT_MESH_b = rawRGB & 0xFF;
|
||||
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||
|
||||
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
|
||||
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||
// Only validate the combined value once
|
||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||
// Extract each component as a normal int first
|
||||
int r = (rawRGB >> 16) & 0xFF;
|
||||
int g = (rawRGB >> 8) & 0xFF;
|
||||
int b = rawRGB & 0xFF;
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,10 +536,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
void Screen::setup()
|
||||
{
|
||||
|
||||
// === Enable display rendering ===
|
||||
// Enable display rendering
|
||||
useDisplay = true;
|
||||
|
||||
// === Load saved brightness from UI config ===
|
||||
// Load saved brightness from UI config
|
||||
// For OLED displays (SSD1306), default brightness is 255 if not set
|
||||
if (uiconfig.screen_brightness == 0) {
|
||||
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
@@ -580,7 +551,7 @@ void Screen::setup()
|
||||
brightness = uiconfig.screen_brightness;
|
||||
}
|
||||
|
||||
// === Detect OLED subtype (if supported by board variant) ===
|
||||
// Detect OLED subtype (if supported by board variant)
|
||||
#ifdef AutoOLEDWire_h
|
||||
if (isAUTOOled)
|
||||
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
||||
@@ -602,7 +573,7 @@ void Screen::setup()
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#endif
|
||||
|
||||
// === Initialize display and UI system ===
|
||||
// Initialize display and UI system
|
||||
ui->init();
|
||||
displayWidth = dispdev->width();
|
||||
displayHeight = dispdev->height();
|
||||
@@ -614,7 +585,7 @@ void Screen::setup()
|
||||
ui->disableAllIndicators(); // Disable page indicator dots
|
||||
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
||||
|
||||
// === Apply loaded brightness ===
|
||||
// Apply loaded brightness
|
||||
#if defined(ST7789_CS)
|
||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
||||
@@ -622,20 +593,20 @@ void Screen::setup()
|
||||
#endif
|
||||
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||
|
||||
// === Set custom overlay callbacks ===
|
||||
// Set custom overlay callbacks
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
|
||||
// === Enable UTF-8 to display mapping ===
|
||||
// Enable UTF-8 to display mapping
|
||||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
logo_timeout *= 2; // Give more time for branded boot logos
|
||||
#endif
|
||||
|
||||
// === Configure alert frames (e.g., "Resuming..." or region name) ===
|
||||
// Configure alert frames (e.g., "Resuming..." or region name)
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
||||
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -651,10 +622,10 @@ void Screen::setup()
|
||||
ui->setFrames(alertFrames, 1);
|
||||
ui->disableAutoTransition(); // Require manual navigation between frames
|
||||
|
||||
// === Log buffer for on-screen logs (3 lines max) ===
|
||||
// Log buffer for on-screen logs (3 lines max)
|
||||
dispdev->setLogBuffer(3, 32);
|
||||
|
||||
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
|
||||
// Optional screen mirroring or flipping (e.g. for T-Beam orientation)
|
||||
#ifdef SCREEN_MIRROR
|
||||
dispdev->mirrorScreen();
|
||||
#else
|
||||
@@ -672,7 +643,7 @@ void Screen::setup()
|
||||
}
|
||||
#endif
|
||||
|
||||
// === Generate device ID from MAC address ===
|
||||
// Generate device ID from MAC address
|
||||
uint8_t dmac[6];
|
||||
getMacAddr(dmac);
|
||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||
@@ -681,7 +652,7 @@ void Screen::setup()
|
||||
handleSetOn(false); // Ensure proper init for Arduino targets
|
||||
#endif
|
||||
|
||||
// === Turn on display and trigger first draw ===
|
||||
// Turn on display and trigger first draw
|
||||
handleSetOn(true);
|
||||
determineResolution(dispdev->height(), dispdev->width());
|
||||
ui->update();
|
||||
@@ -704,7 +675,7 @@ void Screen::setup()
|
||||
touchScreenImpl1->init();
|
||||
#endif
|
||||
|
||||
// === Subscribe to device status updates ===
|
||||
// Subscribe to device status updates
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
||||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||||
@@ -712,12 +683,14 @@ void Screen::setup()
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe(adminModule);
|
||||
#endif
|
||||
if (textMessageModule)
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
if (inputBroker)
|
||||
inputObserver.observe(inputBroker);
|
||||
|
||||
// === Notify modules that support UI events ===
|
||||
// Load persisted messages into RAM
|
||||
messageStore.loadFromFlash();
|
||||
LOG_INFO("MessageStore loaded from flash");
|
||||
|
||||
// Notify modules that support UI events
|
||||
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
||||
}
|
||||
|
||||
@@ -788,6 +761,23 @@ int32_t Screen::runOnce()
|
||||
if (displayHeight == 0) {
|
||||
displayHeight = dispdev->getHeight();
|
||||
}
|
||||
|
||||
// Detect frame transitions and clear message cache when leaving text message screen
|
||||
{
|
||||
static int8_t lastFrameIndex = -1;
|
||||
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
|
||||
int8_t textMsgIndex = framesetInfo.positions.textMessage;
|
||||
|
||||
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
|
||||
|
||||
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
}
|
||||
}
|
||||
|
||||
lastFrameIndex = currentFrameIndex;
|
||||
}
|
||||
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
|
||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||
@@ -843,17 +833,17 @@ int32_t Screen::runOnce()
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
showFrame(FrameDirection::NEXT);
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
}
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
showFrame(FrameDirection::NEXT);
|
||||
}
|
||||
break;
|
||||
case Cmd::START_ALERT_FRAME: {
|
||||
@@ -874,6 +864,7 @@ int32_t Screen::runOnce()
|
||||
break;
|
||||
case Cmd::STOP_ALERT_FRAME:
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
break;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
@@ -1044,9 +1035,6 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||||
|
||||
if (!hiddenFrames.home) {
|
||||
fsi.positions.home = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||
@@ -1058,11 +1046,16 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (!hiddenFrames.nodelist) {
|
||||
fsi.positions.nodelist = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||
if (!hiddenFrames.nodelist_nodes) {
|
||||
fsi.positions.nodelist_nodes = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
}
|
||||
if (!hiddenFrames.nodelist_location) {
|
||||
fsi.positions.nodelist_location = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Show detailed node views only on E-Ink builds
|
||||
@@ -1084,11 +1077,13 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (!hiddenFrames.nodelist_bearings) {
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
}
|
||||
#endif
|
||||
if (!hiddenFrames.gps) {
|
||||
fsi.positions.gps = numframes;
|
||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||
@@ -1188,7 +1183,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
|
||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
||||
this->frameCount = numframes; // Save frame count for use in custom overlay
|
||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||
|
||||
ui->setFrames(normalFrames, numframes);
|
||||
@@ -1208,10 +1203,6 @@ void Screen::setFrames(FrameFocus focus)
|
||||
case FOCUS_FAULT:
|
||||
ui->switchToFrame(fsi.positions.fault);
|
||||
break;
|
||||
case FOCUS_TEXTMESSAGE:
|
||||
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
|
||||
ui->switchToFrame(fsi.positions.textMessage);
|
||||
break;
|
||||
case FOCUS_MODULE:
|
||||
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
||||
// If no module requested focus, will show the first frame instead
|
||||
@@ -1254,8 +1245,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
||||
void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist") {
|
||||
hiddenFrames.nodelist = !hiddenFrames.nodelist;
|
||||
if (frameName == "nodelist_nodes") {
|
||||
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes;
|
||||
}
|
||||
if (frameName == "nodelist_location") {
|
||||
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
@@ -1270,9 +1264,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
}
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_bearings") {
|
||||
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
||||
}
|
||||
#endif
|
||||
if (frameName == "gps") {
|
||||
hiddenFrames.gps = !hiddenFrames.gps;
|
||||
}
|
||||
@@ -1294,8 +1290,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
{
|
||||
#ifndef USE_EINK
|
||||
if (frameName == "nodelist")
|
||||
return hiddenFrames.nodelist;
|
||||
if (frameName == "nodelist_nodes")
|
||||
return hiddenFrames.nodelist_nodes;
|
||||
if (frameName == "nodelist_location")
|
||||
return hiddenFrames.nodelist_location;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_lastheard")
|
||||
@@ -1306,8 +1304,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
return hiddenFrames.nodelist_distance;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
if (frameName == "nodelist_bearings")
|
||||
return hiddenFrames.nodelist_bearings;
|
||||
#endif
|
||||
if (frameName == "gps")
|
||||
return hiddenFrames.gps;
|
||||
#endif
|
||||
@@ -1323,37 +1323,6 @@ bool Screen::isFrameHidden(const std::string &frameName) const
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dismisses the currently displayed screen frame, if possible
|
||||
// Relevant for text message, waypoint, others in future?
|
||||
// Triggered with a CardKB keycombo
|
||||
void Screen::hideCurrentFrame()
|
||||
{
|
||||
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
bool dismissed = false;
|
||||
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||||
LOG_INFO("Hide Text Message");
|
||||
devicestate.has_rx_text_message = false;
|
||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||||
LOG_DEBUG("Hide Waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
hiddenFrames.waypoint = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||||
LOG_DEBUG("Hide WiFi Screen");
|
||||
hiddenFrames.wifi = true;
|
||||
dismissed = true;
|
||||
} else if (currentFrame == framesetInfo.positions.lora) {
|
||||
LOG_INFO("Hide LoRa");
|
||||
hiddenFrames.lora = true;
|
||||
dismissed = true;
|
||||
}
|
||||
|
||||
if (dismissed) {
|
||||
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::handleStartFirmwareUpdateScreen()
|
||||
{
|
||||
LOG_DEBUG("Show firmware screen");
|
||||
@@ -1406,28 +1375,6 @@ void Screen::decreaseBrightness()
|
||||
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/
|
||||
}
|
||||
|
||||
void Screen::setFunctionSymbol(std::string sym)
|
||||
{
|
||||
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
|
||||
functionSymbol.push_back(sym);
|
||||
functionSymbolString = "";
|
||||
for (auto symbol : functionSymbol) {
|
||||
functionSymbolString = symbol + " " + functionSymbolString;
|
||||
}
|
||||
setFastFramerate();
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::removeFunctionSymbol(std::string sym)
|
||||
{
|
||||
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
|
||||
functionSymbolString = "";
|
||||
for (auto symbol : functionSymbol) {
|
||||
functionSymbolString = symbol + " " + functionSymbolString;
|
||||
}
|
||||
setFastFramerate();
|
||||
}
|
||||
|
||||
void Screen::handleOnPress()
|
||||
{
|
||||
// If screen was off, just wake it, otherwise advance to next frame
|
||||
@@ -1439,23 +1386,17 @@ void Screen::handleOnPress()
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::handleShowPrevFrame()
|
||||
void Screen::showFrame(FrameDirection direction)
|
||||
{
|
||||
// If screen was off, just wake it, otherwise go back to previous frame
|
||||
// If we are in a transition, the press must have bounced, drop it.
|
||||
// Only advance frames when UI is stable
|
||||
if (ui->getUiState()->frameState == FIXED) {
|
||||
ui->previousFrame();
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::handleShowNextFrame()
|
||||
{
|
||||
// If screen was off, just wake it, otherwise advance to next frame
|
||||
// If we are in a transition, the press must have bounced, drop it.
|
||||
if (ui->getUiState()->frameState == FIXED) {
|
||||
ui->nextFrame();
|
||||
if (direction == FrameDirection::NEXT) {
|
||||
ui->nextFrame();
|
||||
} else {
|
||||
ui->previousFrame();
|
||||
}
|
||||
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
}
|
||||
@@ -1481,7 +1422,6 @@ void Screen::setFastFramerate()
|
||||
|
||||
int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
||||
{
|
||||
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
|
||||
switch (arg->getStatusType()) {
|
||||
case STATUS_TYPE_NODE:
|
||||
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
|
||||
@@ -1513,14 +1453,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());
|
||||
@@ -1544,38 +1484,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1593,16 +1579,26 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||
|
||||
if (showingNormalScreen) {
|
||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) {
|
||||
setFrames(FOCUS_MODULE);
|
||||
}
|
||||
|
||||
// Regenerate the frameset, while Attempt to maintain focus on the current frame
|
||||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
|
||||
// Regenerate the frameset, while attempting to maintain focus on the current frame
|
||||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) {
|
||||
setFrames(FOCUS_PRESERVE);
|
||||
}
|
||||
|
||||
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
||||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
|
||||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) {
|
||||
setFastFramerate();
|
||||
}
|
||||
|
||||
// Jump directly to the Text Message screen
|
||||
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
|
||||
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
|
||||
ui->switchToFrame(framesetInfo.positions.textMessage);
|
||||
setFastFramerate(); // force redraw ASAP
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -1640,7 +1636,48 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
return 0;
|
||||
}
|
||||
// UP/DOWN in message screen scrolls through message threads
|
||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollUp();
|
||||
setFastFramerate(); // match existing behavior
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||
if (messageStore.getMessages().empty()) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else {
|
||||
graphics::MessageRenderer::scrollDown();
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// UP/DOWN in node list screens scrolls through node pages
|
||||
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
||||
if (event->inputEvent == INPUT_BROKER_UP) {
|
||||
graphics::NodeListRenderer::scrollUp();
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (event->inputEvent == INPUT_BROKER_DOWN) {
|
||||
graphics::NodeListRenderer::scrollDown();
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// Use left or right input from a keyboard to move between frames,
|
||||
// so long as a mesh module isn't using these events for some other purpose
|
||||
if (showingNormalScreen) {
|
||||
@@ -1654,10 +1691,39 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
|
||||
// If no modules are using the input, move between frames
|
||||
if (!inputIntercepted) {
|
||||
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
|
||||
bool handledEncoderScroll = false;
|
||||
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
||||
!messageStore.getMessages().empty());
|
||||
if (isTextMessageFrame) {
|
||||
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(-1);
|
||||
handledEncoderScroll = true;
|
||||
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
graphics::MessageRenderer::nudgeScroll(1);
|
||||
handledEncoderScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handledEncoderScroll) {
|
||||
setFastFramerate();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
showPrevFrame();
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||
showFrame(FrameDirection::NEXT);
|
||||
} 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_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
@@ -1672,7 +1738,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||
menuHandler::loraMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||
if (devicestate.rx_text_message.from) {
|
||||
if (!messageStore.getMessages().empty()) {
|
||||
menuHandler::messageResponseMenu();
|
||||
} else {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -1685,7 +1751,8 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||
menuHandler::favoriteBaseMenu();
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||
@@ -1696,7 +1763,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
menuHandler::wifiBaseMenu();
|
||||
}
|
||||
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||
showPrevFrame();
|
||||
showFrame(FrameDirection::PREVIOUS);
|
||||
} else if (event->inputEvent == INPUT_BROKER_CANCEL) {
|
||||
setOn(false);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ class Screen
|
||||
FOCUS_DEFAULT, // No specific frame
|
||||
FOCUS_PRESERVE, // Return to the previous frame
|
||||
FOCUS_FAULT,
|
||||
FOCUS_TEXTMESSAGE,
|
||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||
FOCUS_CLOCK,
|
||||
FOCUS_SYSTEM,
|
||||
@@ -55,8 +54,6 @@ class Screen
|
||||
void startFirmwareUpdateScreen() {}
|
||||
void increaseBrightness() {}
|
||||
void decreaseBrightness() {}
|
||||
void setFunctionSymbol(std::string) {}
|
||||
void removeFunctionSymbol(std::string) {}
|
||||
void startAlert(const char *) {}
|
||||
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
|
||||
void showOverlayBanner(BannerOverlayOptions) {}
|
||||
@@ -172,6 +169,8 @@ class Point
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
enum class FrameDirection { NEXT, PREVIOUS };
|
||||
|
||||
// Forward declarations
|
||||
class Screen;
|
||||
|
||||
@@ -211,8 +210,6 @@ class Screen : public concurrency::OSThread
|
||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
|
||||
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
|
||||
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
|
||||
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
|
||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||
@@ -223,6 +220,10 @@ class Screen : public concurrency::OSThread
|
||||
public:
|
||||
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||
|
||||
// Screen dimension accessors
|
||||
inline int getHeight() const { return displayHeight; }
|
||||
inline int getWidth() const { return displayWidth; }
|
||||
size_t frameCount = 0; // Total number of active frames
|
||||
~Screen();
|
||||
|
||||
@@ -231,7 +232,6 @@ class Screen : public concurrency::OSThread
|
||||
FOCUS_DEFAULT, // No specific frame
|
||||
FOCUS_PRESERVE, // Return to the previous frame
|
||||
FOCUS_FAULT,
|
||||
FOCUS_TEXTMESSAGE,
|
||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||
FOCUS_CLOCK,
|
||||
FOCUS_SYSTEM,
|
||||
@@ -279,6 +279,7 @@ class Screen : public concurrency::OSThread
|
||||
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
|
||||
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
|
||||
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
|
||||
void showFrame(FrameDirection direction);
|
||||
|
||||
// generic alert start
|
||||
void startAlert(FrameCallback _alertFrame)
|
||||
@@ -346,9 +347,6 @@ class Screen : public concurrency::OSThread
|
||||
void increaseBrightness();
|
||||
void decreaseBrightness();
|
||||
|
||||
void setFunctionSymbol(std::string sym);
|
||||
void removeFunctionSymbol(std::string sym);
|
||||
|
||||
/// Stops showing the boot screen.
|
||||
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
||||
|
||||
@@ -579,7 +577,7 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Handle observer events
|
||||
int handleStatusUpdate(const meshtastic::Status *arg);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
||||
int handleTextMessage(const meshtastic_MeshPacket *packet);
|
||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||
int handleInputEvent(const InputEvent *arg);
|
||||
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||
@@ -590,9 +588,6 @@ class Screen : public concurrency::OSThread
|
||||
/// Draws our SSL cert screen during boot (called from WebServer)
|
||||
void setSSLFrames();
|
||||
|
||||
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||
void hideCurrentFrame();
|
||||
|
||||
// Menu-driven Show / Hide Toggle
|
||||
void toggleFrameVisibility(const std::string &frameName);
|
||||
bool isFrameHidden(const std::string &frameName) const;
|
||||
@@ -640,8 +635,6 @@ class Screen : public concurrency::OSThread
|
||||
// Implementations of various commands, called from doTask().
|
||||
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
|
||||
void handleOnPress();
|
||||
void handleShowNextFrame();
|
||||
void handleShowPrevFrame();
|
||||
void handleStartFirmwareUpdateScreen();
|
||||
|
||||
// Info collected by setFrames method.
|
||||
@@ -661,7 +654,8 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
uint8_t nodelist_lastheard = 255;
|
||||
uint8_t nodelist_hopsignal = 255;
|
||||
uint8_t nodelist_distance = 255;
|
||||
@@ -684,7 +678,8 @@ class Screen : public concurrency::OSThread
|
||||
bool home = false;
|
||||
bool clock = false;
|
||||
#ifndef USE_EINK
|
||||
bool nodelist = false;
|
||||
bool nodelist_nodes = false;
|
||||
bool nodelist_location = false;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
bool nodelist_lastheard = false;
|
||||
@@ -692,7 +687,9 @@ class Screen : public concurrency::OSThread
|
||||
bool nodelist_distance = false;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
#ifdef USE_EINK
|
||||
bool nodelist_bearings = false;
|
||||
#endif
|
||||
bool gps = false;
|
||||
#endif
|
||||
bool lora = false;
|
||||
|
||||
@@ -32,6 +32,19 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||
}
|
||||
}
|
||||
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
|
||||
{
|
||||
hour = 0;
|
||||
minute = 0;
|
||||
second = 0;
|
||||
if (rtc_sec == 0)
|
||||
return;
|
||||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = hms % SEC_PER_MIN;
|
||||
}
|
||||
|
||||
// === Shared External State ===
|
||||
bool hasUnreadMessage = false;
|
||||
bool isMuted = false;
|
||||
@@ -200,8 +213,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
|
||||
// === Build Date String ===
|
||||
@@ -414,8 +427,12 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
|
||||
if (drawConnectionState) {
|
||||
const int scale = isHighResolution ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (isHighResolution) {
|
||||
const int scale = 2;
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
@@ -45,6 +45,8 @@ extern bool isMuted;
|
||||
extern bool isHighResolution;
|
||||
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
|
||||
|
||||
// Rounded highlight (used for inverted headers)
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||
|
||||
|
||||
@@ -354,8 +354,6 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
|
||||
if (screenHeight <= 64) {
|
||||
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||
} else {
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -23,6 +18,31 @@ namespace graphics
|
||||
namespace ClockRenderer
|
||||
{
|
||||
|
||||
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
|
||||
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
|
||||
// Segment layout reference:
|
||||
//
|
||||
// ___1___
|
||||
// 6 | | 2
|
||||
// |_7___|
|
||||
// 5 | | 3
|
||||
// |___4_|
|
||||
//
|
||||
// Segment order: [1, 2, 3, 4, 5, 6, 7]
|
||||
//
|
||||
static const uint8_t PROGMEM digitSegments[10][7] = {
|
||||
{1, 1, 1, 1, 1, 1, 0}, // 0
|
||||
{0, 1, 1, 0, 0, 0, 0}, // 1
|
||||
{1, 1, 0, 1, 1, 0, 1}, // 2
|
||||
{1, 1, 1, 1, 0, 0, 1}, // 3
|
||||
{0, 1, 1, 0, 0, 1, 1}, // 4
|
||||
{1, 0, 1, 1, 0, 1, 1}, // 5
|
||||
{1, 0, 1, 1, 1, 1, 1}, // 6
|
||||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||
{1, 1, 1, 1, 1, 1, 1}, // 8
|
||||
{1, 1, 1, 1, 0, 1, 1} // 9
|
||||
};
|
||||
|
||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
{
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
@@ -30,7 +50,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
|
||||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||
|
||||
uint16_t topAndBottomX = x + (4 * scale);
|
||||
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
|
||||
|
||||
uint16_t quarterCellHeight = cellHeight / 4;
|
||||
|
||||
@@ -43,34 +63,16 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
|
||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||||
{
|
||||
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
|
||||
// segment {innerIndex + 1}
|
||||
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
|
||||
uint8_t numbers[10][7] = {
|
||||
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
|
||||
{0, 1, 1, 0, 0, 0, 0}, // 1 1
|
||||
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
|
||||
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
|
||||
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
|
||||
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
|
||||
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
|
||||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||||
{1, 1, 1, 1, 1, 1, 1}, // 8 4
|
||||
{1, 1, 1, 1, 0, 1, 1}, // 9
|
||||
};
|
||||
|
||||
// the width and height of each segment's central rectangle:
|
||||
// _____________________
|
||||
// ⋰| (only this part, |⋱
|
||||
// ⋰ | not including | ⋱
|
||||
// ⋱ | the triangles | ⋰
|
||||
// ⋱| on the ends) |⋰
|
||||
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Read 7-segment pattern for the digit from flash
|
||||
uint8_t seg[7];
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
||||
}
|
||||
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
// segment x and y coordinates
|
||||
// Precompute segment positions
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
@@ -92,33 +94,21 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
|
||||
uint16_t segmentSevenX = segmentOneX;
|
||||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
if (numbers[number][0]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][1]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][2]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][3]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][4]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][5]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][6]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
// Draw only the active segments
|
||||
if (seg[0])
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
if (seg[1])
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
if (seg[2])
|
||||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
if (seg[3])
|
||||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
if (seg[4])
|
||||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
if (seg[5])
|
||||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
if (seg[6])
|
||||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||
@@ -147,42 +137,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||
}
|
||||
|
||||
/*
|
||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||
{
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
if (digitalMode) {
|
||||
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
|
||||
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
|
||||
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
|
||||
|
||||
display->drawCircle(centerX, centerY, radius);
|
||||
display->drawCircle(centerX, centerY, radius + 1);
|
||||
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
|
||||
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
|
||||
} else {
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentThreeX = segmentOneX;
|
||||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
uint16_t segmentFourX = x;
|
||||
uint16_t segmentFourY = y + segmentHeight + 2;
|
||||
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
}
|
||||
}
|
||||
*/
|
||||
// Draw a digital clock
|
||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
@@ -192,7 +146,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
@@ -268,10 +221,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
scaleInitialized = true;
|
||||
}
|
||||
|
||||
size_t len = strlen(timeString);
|
||||
|
||||
// calculate hours:minutes string width
|
||||
uint16_t timeStringWidth = len * 5; // base spacing between characters
|
||||
size_t len = strlen(timeString);
|
||||
uint16_t timeStringWidth = len * 5;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
@@ -339,19 +291,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
int16_t centerY = display->getHeight() / 2;
|
||||
|
||||
// clock face radius
|
||||
int16_t radius = 0;
|
||||
if (display->getHeight() < display->getWidth()) {
|
||||
radius = (display->getHeight() / 2) * 0.9;
|
||||
} else {
|
||||
radius = (display->getWidth() / 2) * 0.9;
|
||||
}
|
||||
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
||||
#ifdef T_WATCH_S3
|
||||
radius = (display->getWidth() / 2) * 0.8;
|
||||
#endif
|
||||
@@ -366,17 +312,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
// tick mark outer y coordinate; (first nested circle)
|
||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||
|
||||
// seconds tick mark inner y coordinate; (second nested circle)
|
||||
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||
if (isHighResolution) {
|
||||
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||
}
|
||||
|
||||
// hours tick mark inner y coordinate; (third nested circle)
|
||||
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||
if (isHighResolution) {
|
||||
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||
}
|
||||
double secondsTickMarkInnerNoonY = noonY + (isHighResolution ? 8 : 4);
|
||||
double hoursTickMarkInnerNoonY = noonY + (isHighResolution ? 16 : 6);
|
||||
|
||||
// minute hand y coordinate
|
||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||
@@ -396,17 +333,11 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
decomposeTime(rtc_sec, hour, minute, second);
|
||||
|
||||
// Tear apart hms into h:m:s
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
if (config.display.use_12h_clock) {
|
||||
isPM = hour >= 12;
|
||||
bool isPM = hour >= 12;
|
||||
display->setFont(FONT_SMALL);
|
||||
int yOffset = isHighResolution ? 1 : 0;
|
||||
#ifdef USE_EINK
|
||||
|
||||
@@ -301,9 +301,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into h:m:s
|
||||
int hour = hms / SEC_PER_HOUR;
|
||||
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
int hour, min, sec;
|
||||
graphics::decomposeTime(rtc_sec, hour, min, sec);
|
||||
|
||||
char timebuf[12];
|
||||
|
||||
@@ -532,8 +531,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
|
||||
@@ -574,7 +575,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 ===
|
||||
@@ -705,10 +706,22 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (isHighResolution) {
|
||||
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
|
||||
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
|
||||
textX_offset = textX_offset * 4;
|
||||
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
|
||||
const int scale = 2;
|
||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||
|
||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
#include "graphics/draw/DebugRenderer.h"
|
||||
#include "graphics/draw/NodeListRenderer.h"
|
||||
#include "graphics/draw/ScreenRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
|
||||
namespace graphics
|
||||
@@ -30,8 +29,6 @@ using namespace ClockRenderer;
|
||||
using namespace CompassRenderer;
|
||||
using namespace DebugRenderer;
|
||||
using namespace NodeListRenderer;
|
||||
using namespace ScreenRenderer;
|
||||
using namespace UIRenderer;
|
||||
|
||||
} // namespace DrawRenderers
|
||||
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "Default.h"
|
||||
#include "GPS.h"
|
||||
#include "MenuHandler.h"
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "buzz.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#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"
|
||||
@@ -406,27 +410,35 @@ void menuHandler::clockMenu()
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::messageResponseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
|
||||
#else
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
|
||||
#endif
|
||||
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
|
||||
int options = 3;
|
||||
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd };
|
||||
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "Reply via Freetext";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// New Reply submenu (replaces Preset and Freetext directly in this menu)
|
||||
optionsArray[options] = "Reply";
|
||||
optionsEnumArray[options++] = ReplyMenu;
|
||||
|
||||
optionsArray[options] = "View Chats";
|
||||
optionsEnumArray[options++] = ViewMode;
|
||||
|
||||
// Delete submenu
|
||||
optionsArray[options] = "Delete";
|
||||
optionsEnumArray[options++] = 900;
|
||||
|
||||
#ifdef HAS_I2S
|
||||
optionsArray[options] = "Read Aloud";
|
||||
optionsEnumArray[options++] = Aloud;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Message";
|
||||
@@ -437,29 +449,376 @@ void menuHandler::messageResponseMenu()
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Dismiss) {
|
||||
screen->hideCurrentFrame();
|
||||
} else if (selected == Preset) {
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
LOG_DEBUG("messageResponseMenu: selected %d", selected);
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer);
|
||||
|
||||
if (selected == ViewMode) {
|
||||
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Reply submenu
|
||||
} else if (selected == ReplyMenu) {
|
||||
menuHandler::menuQueue = menuHandler::reply_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Delete submenu
|
||||
} else if (selected == 900) {
|
||||
menuHandler::menuQueue = menuHandler::delete_messages_menu;
|
||||
screen->runNow();
|
||||
|
||||
// Delete oldest FIRST (only change)
|
||||
} else if (selected == DeleteOldest) {
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
// Global oldest
|
||||
messageStore.deleteOldestMessage();
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
// Oldest in current channel
|
||||
messageStore.deleteOldestMessageInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
// Oldest in current DM
|
||||
messageStore.deleteOldestMessageWithPeer(peer);
|
||||
}
|
||||
} else if (selected == Freetext) {
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all messages
|
||||
} else if (selected == DeleteAll) {
|
||||
messageStore.clearAllMessages();
|
||||
graphics::MessageRenderer::clearThreadRegistries();
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
|
||||
#ifdef HAS_I2S
|
||||
else if (selected == Aloud) {
|
||||
} else if (selected == Aloud) {
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||
|
||||
audioThread->readAloud(msg);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::replyMenu()
|
||||
{
|
||||
enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
// Back
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
// Preset reply
|
||||
optionsArray[options] = "With Preset";
|
||||
optionsEnumArray[options++] = ReplyPreset;
|
||||
|
||||
// Freetext reply (only when keyboard exists)
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "With Freetext";
|
||||
optionsEnumArray[options++] = ReplyFreetext;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
|
||||
// Dynamic title based on thread mode
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
bannerOptions.message = "Reply to Channel";
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
bannerOptions.message = "Reply to DM";
|
||||
} else {
|
||||
// View All
|
||||
bannerOptions.message = "Reply to Last Msg";
|
||||
}
|
||||
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.InitialSelected = 1;
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Preset reply
|
||||
if (selected == ReplyPreset) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Freetext reply
|
||||
if (selected == ReplyFreetext) {
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
|
||||
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(peer);
|
||||
|
||||
} else {
|
||||
// Fallback for last received message
|
||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||
} else {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
void menuHandler::deleteMessagesMenu()
|
||||
{
|
||||
enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd];
|
||||
static int optionsEnumArray[enumEnd];
|
||||
int options = 0;
|
||||
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
|
||||
optionsArray[options] = "Back";
|
||||
optionsEnumArray[options++] = Back;
|
||||
|
||||
optionsArray[options] = "Delete Oldest";
|
||||
optionsEnumArray[options++] = DeleteOldest;
|
||||
|
||||
// If viewing ALL chats → hide “Delete This Chat”
|
||||
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
optionsArray[options] = "Delete This Chat";
|
||||
optionsEnumArray[options++] = DeleteThis;
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Delete All";
|
||||
#else
|
||||
optionsArray[options] = "Delete All Chats";
|
||||
#endif
|
||||
optionsEnumArray[options++] = DeleteAll;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Delete Messages";
|
||||
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.bannerCallback = [mode](int selected) -> void {
|
||||
int ch = graphics::MessageRenderer::getThreadChannel();
|
||||
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
|
||||
|
||||
if (selected == Back) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected == DeleteAll) {
|
||||
LOG_INFO("Deleting all messages");
|
||||
messageStore.clearAllMessages();
|
||||
graphics::MessageRenderer::clearThreadRegistries();
|
||||
graphics::MessageRenderer::clearMessageCache();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected == DeleteOldest) {
|
||||
LOG_INFO("Deleting oldest message");
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
|
||||
messageStore.deleteOldestMessage();
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
messageStore.deleteOldestMessageInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
messageStore.deleteOldestMessageWithPeer(peer);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This only appears in non-ALL modes
|
||||
if (selected == DeleteThis) {
|
||||
LOG_INFO("Deleting all messages in this thread");
|
||||
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
|
||||
messageStore.deleteAllMessagesInChannel(ch);
|
||||
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
messageStore.deleteAllMessagesWithPeer(peer);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::messageViewModeMenu()
|
||||
{
|
||||
auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
|
||||
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
|
||||
|
||||
static std::vector<std::string> labels;
|
||||
static std::vector<int> ids;
|
||||
static std::vector<uint32_t> idToPeer; // DM lookup
|
||||
|
||||
labels.clear();
|
||||
ids.clear();
|
||||
idToPeer.clear();
|
||||
|
||||
labels.push_back("Back");
|
||||
ids.push_back(-1);
|
||||
labels.push_back("View All Chats");
|
||||
ids.push_back(-2);
|
||||
|
||||
// Channels with messages
|
||||
for (int ch = 0; ch < 8; ++ch) {
|
||||
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
|
||||
if (!msgs.empty()) {
|
||||
char buf[40];
|
||||
const char *cname = channels.getName(ch);
|
||||
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
|
||||
labels.push_back(buf);
|
||||
ids.push_back(encodeChannelId(ch));
|
||||
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
|
||||
}
|
||||
}
|
||||
|
||||
// Registry channels
|
||||
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
|
||||
if (ch < 0 || ch >= 8)
|
||||
continue;
|
||||
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
|
||||
if (msgs.empty())
|
||||
continue;
|
||||
int enc = encodeChannelId(ch);
|
||||
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
|
||||
char buf[40];
|
||||
const char *cname = channels.getName(ch);
|
||||
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
|
||||
labels.push_back(buf);
|
||||
ids.push_back(enc);
|
||||
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
|
||||
}
|
||||
}
|
||||
|
||||
// Gather unique peers
|
||||
auto dms = messageStore.getDirectMessages();
|
||||
std::vector<uint32_t> uniquePeers;
|
||||
for (auto &m : dms) {
|
||||
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
|
||||
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
|
||||
uniquePeers.push_back(peer);
|
||||
}
|
||||
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
|
||||
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
|
||||
uniquePeers.push_back(peer);
|
||||
}
|
||||
std::sort(uniquePeers.begin(), uniquePeers.end());
|
||||
|
||||
// Encode peers
|
||||
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
||||
uint32_t peer = uniquePeers[i];
|
||||
auto node = nodeDB->getMeshNode(peer);
|
||||
std::string name;
|
||||
if (node && node->has_user)
|
||||
name = sanitizeString(node->user.long_name).substr(0, 15);
|
||||
else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
||||
name = buf;
|
||||
}
|
||||
labels.push_back("@" + name);
|
||||
int encPeer = 1000 + (int)idToPeer.size();
|
||||
ids.push_back(encPeer);
|
||||
idToPeer.push_back(peer);
|
||||
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
|
||||
}
|
||||
|
||||
// Active ID
|
||||
int activeId = -2;
|
||||
auto mode = graphics::MessageRenderer::getThreadMode();
|
||||
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
|
||||
activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
|
||||
else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
uint32_t cur = graphics::MessageRenderer::getThreadPeer();
|
||||
for (size_t i = 0; i < idToPeer.size(); ++i)
|
||||
if (idToPeer[i] == cur) {
|
||||
activeId = 1000 + (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
|
||||
|
||||
// Build banner
|
||||
static std::vector<const char *> options;
|
||||
static std::vector<int> optionIds;
|
||||
options.clear();
|
||||
optionIds.clear();
|
||||
|
||||
int initialIndex = 0;
|
||||
for (size_t i = 0; i < labels.size(); i++) {
|
||||
options.push_back(labels[i].c_str());
|
||||
optionIds.push_back(ids[i]);
|
||||
if (ids[i] == activeId)
|
||||
initialIndex = (int)i;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Select Conversation";
|
||||
bannerOptions.optionsArrayPtr = options.data();
|
||||
bannerOptions.optionsEnumPtr = optionIds.data();
|
||||
bannerOptions.optionsCount = options.size();
|
||||
bannerOptions.InitialSelected = initialIndex;
|
||||
|
||||
bannerOptions.bannerCallback = [=](int selected) -> void {
|
||||
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
|
||||
if (selected == -1) {
|
||||
menuHandler::menuQueue = menuHandler::message_response_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == -2) {
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
|
||||
} else if (isChannelSel(selected)) {
|
||||
int ch = selected - 100;
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
|
||||
} else if (selected >= 1000) {
|
||||
int idx = selected - 1000;
|
||||
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
|
||||
uint32_t peer = idToPeer[idx];
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
|
||||
}
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
@@ -485,16 +844,6 @@ void menuHandler::homeBaseMenu()
|
||||
optionsArray[options] = "Send Node Info";
|
||||
}
|
||||
optionsEnumArray[options++] = Position;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "New Preset";
|
||||
#else
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
#endif
|
||||
optionsEnumArray[options++] = Preset;
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -650,19 +999,37 @@ void menuHandler::systemBaseMenu()
|
||||
|
||||
void menuHandler::favoriteBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
|
||||
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd };
|
||||
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show "View Conversation" if a message exists with this node
|
||||
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
|
||||
bool hasConversation = false;
|
||||
for (const auto &m : messageStore.getMessages()) {
|
||||
if ((m.sender == peer || m.dest == peer)) {
|
||||
hasConversation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasConversation) {
|
||||
optionsArray[options] = "Go To Chat";
|
||||
optionsEnumArray[options++] = GoToChat;
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
|
||||
optionsArray[options] = "New Preset";
|
||||
#else
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
#endif
|
||||
static int optionsEnumArray[enumEnd] = {Back, Preset};
|
||||
int options = 2;
|
||||
optionsEnumArray[options++] = Preset;
|
||||
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
@@ -684,6 +1051,17 @@ void menuHandler::favoriteBaseMenu()
|
||||
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
} else if (selected == Freetext) {
|
||||
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
}
|
||||
// Handle new Go To Thread action
|
||||
else if (selected == GoToChat) {
|
||||
// Switch thread to direct conversation with this node
|
||||
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
|
||||
graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
|
||||
// Manually create and send a UIFrameEvent to trigger the jump
|
||||
UIFrameEvent evt;
|
||||
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
|
||||
screen->handleUIFrameEvent(&evt);
|
||||
} else if (selected == Remove) {
|
||||
menuHandler::menuQueue = menuHandler::remove_favorite;
|
||||
screen->runNow();
|
||||
@@ -733,20 +1111,31 @@ void menuHandler::positionBaseMenu()
|
||||
|
||||
void menuHandler::nodeListMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
|
||||
#else
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd };
|
||||
static const char *optionsArray[enumEnd] = {"Back"};
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
optionsArray[options] = "Add Favorite";
|
||||
optionsEnumArray[options++] = Favorite;
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Key Verification";
|
||||
optionsEnumArray[options++] = Verify;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Show Long/Short Name";
|
||||
optionsEnumArray[options++] = NodeNameLength;
|
||||
optionsArray[options] = "Reset NodeDB";
|
||||
optionsEnumArray[options++] = Reset;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Node Action";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.optionsCount = 3;
|
||||
#else
|
||||
bannerOptions.optionsCount = 5;
|
||||
#endif
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Favorite) {
|
||||
menuQueue = add_favorite;
|
||||
@@ -760,6 +1149,9 @@ void menuHandler::nodeListMenu()
|
||||
} else if (selected == TraceRoute) {
|
||||
menuQueue = trace_route_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -783,7 +1175,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
LOG_INFO("Setting names to short");
|
||||
config.display.use_long_node_name = false;
|
||||
} else if (selected == Back) {
|
||||
menuQueue = screen_options_menu;
|
||||
menuQueue = node_base_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
@@ -811,6 +1203,9 @@ void menuHandler::resetNodeDBMenu()
|
||||
LOG_INFO("Initiate node-db reset but keeping favorites");
|
||||
nodeDB->resetNodes(1);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 0) {
|
||||
menuQueue = node_base_menu;
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
@@ -1040,12 +1435,13 @@ 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 || defined(HACKADAY_COMMUNICATOR)
|
||||
@@ -1081,20 +1477,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();
|
||||
@@ -1148,6 +1564,7 @@ void menuHandler::rebootMenu()
|
||||
if (selected == 1) {
|
||||
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
|
||||
nodeDB->saveToDisk();
|
||||
messageStore.saveToFlash();
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
} else {
|
||||
menuQueue = power_menu;
|
||||
@@ -1352,16 +1769,11 @@ void menuHandler::screenOptionsMenu()
|
||||
hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits };
|
||||
static const char *optionsArray[5] = {"Back"};
|
||||
static int optionsEnumArray[5] = {Back};
|
||||
int options = 1;
|
||||
|
||||
#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR)
|
||||
optionsArray[options] = "Show Long/Short Name";
|
||||
optionsEnumArray[options++] = NodeNameLength;
|
||||
#endif
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
if (hasSupportBrightness) {
|
||||
optionsArray[options] = "Brightness";
|
||||
@@ -1375,7 +1787,7 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsEnumArray[options++] = ScreenColor;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Frame Visibility Toggle";
|
||||
optionsArray[options] = "Frame Visibility";
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
optionsArray[options] = "Display Units";
|
||||
@@ -1393,9 +1805,6 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
||||
screen->runNow();
|
||||
} else if (selected == NodeNameLength) {
|
||||
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
@@ -1491,7 +1900,8 @@ void menuHandler::FrameToggles_menu()
|
||||
{
|
||||
enum optionsNumbers {
|
||||
Finish,
|
||||
nodelist,
|
||||
nodelist_nodes,
|
||||
nodelist_location,
|
||||
nodelist_lastheard,
|
||||
nodelist_hopsignal,
|
||||
nodelist_distance,
|
||||
@@ -1512,20 +1922,25 @@ void menuHandler::FrameToggles_menu()
|
||||
static int lastSelectedIndex = 0;
|
||||
|
||||
#ifndef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
|
||||
optionsEnumArray[options++] = nodelist;
|
||||
#endif
|
||||
#ifdef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists";
|
||||
optionsEnumArray[options++] = nodelist_nodes;
|
||||
#else
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
|
||||
optionsEnumArray[options++] = nodelist_lastheard;
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
|
||||
optionsEnumArray[options++] = nodelist_hopsignal;
|
||||
#endif
|
||||
|
||||
#if HAS_GPS
|
||||
#ifndef USE_EINK
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
|
||||
optionsEnumArray[options++] = nodelist_location;
|
||||
#else
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
|
||||
optionsEnumArray[options++] = nodelist_distance;
|
||||
#endif
|
||||
#if HAS_GPS
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
|
||||
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
|
||||
optionsEnumArray[options++] = nodelist_bearings;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||
optionsEnumArray[options++] = gps;
|
||||
@@ -1564,8 +1979,12 @@ void menuHandler::FrameToggles_menu()
|
||||
|
||||
if (selected == Finish) {
|
||||
screen->setFrames(Screen::FOCUS_DEFAULT);
|
||||
} else if (selected == nodelist) {
|
||||
screen->toggleFrameVisibility("nodelist");
|
||||
} else if (selected == nodelist_nodes) {
|
||||
screen->toggleFrameVisibility("nodelist_nodes");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_location) {
|
||||
screen->toggleFrameVisibility("nodelist_location");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == nodelist_lastheard) {
|
||||
@@ -1681,6 +2100,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case position_base_menu:
|
||||
positionBaseMenu();
|
||||
break;
|
||||
case node_base_menu:
|
||||
nodeListMenu();
|
||||
break;
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
case gps_toggle_menu:
|
||||
GPSToggleMenu();
|
||||
@@ -1761,6 +2183,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case throttle_message:
|
||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||
break;
|
||||
case message_response_menu:
|
||||
messageResponseMenu();
|
||||
break;
|
||||
case reply_menu:
|
||||
replyMenu();
|
||||
break;
|
||||
case delete_messages_menu:
|
||||
deleteMessagesMenu();
|
||||
break;
|
||||
case message_viewmode_menu:
|
||||
messageViewModeMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = menu_none;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class menuHandler
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
compass_point_north_menu,
|
||||
@@ -43,6 +44,10 @@ class menuHandler
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
@@ -61,6 +66,9 @@ class menuHandler
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
static void deleteMessagesMenu();
|
||||
static void homeBaseMenu();
|
||||
static void textMessageBaseMenu();
|
||||
static void systemBaseMenu();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,11 @@
|
||||
#pragma once
|
||||
#include "MessageStore.h" // for StoredMessage
|
||||
#if HAS_SCREEN
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -10,6 +14,27 @@ namespace graphics
|
||||
namespace MessageRenderer
|
||||
{
|
||||
|
||||
// Thread filter modes
|
||||
enum class ThreadMode { ALL, CHANNEL, DIRECT };
|
||||
|
||||
// Setter for switching thread mode
|
||||
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
|
||||
|
||||
// Getter for current mode
|
||||
ThreadMode getThreadMode();
|
||||
|
||||
// Getter for current channel (valid if mode == CHANNEL)
|
||||
int getThreadChannel();
|
||||
|
||||
// Getter for current peer (valid if mode == DIRECT)
|
||||
uint32_t getThreadPeer();
|
||||
|
||||
// Registry accessors for menuHandler
|
||||
const std::vector<int> &getSeenChannels();
|
||||
const std::vector<uint32_t> &getSeenPeers();
|
||||
|
||||
void clearThreadRegistries();
|
||||
|
||||
// Text and emote rendering
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
|
||||
|
||||
@@ -20,11 +45,27 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
|
||||
|
||||
// Function to calculate heights for each line
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
|
||||
const std::vector<bool> &isHeaderVec);
|
||||
|
||||
// Function to render the message content
|
||||
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
|
||||
// Reset scroll state when new messages arrive
|
||||
void resetScrollState();
|
||||
|
||||
// Manual scroll control for encoder-style inputs
|
||||
void nudgeScroll(int8_t direction);
|
||||
|
||||
// Helper to auto-select the correct thread mode from a message
|
||||
void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet);
|
||||
|
||||
// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset
|
||||
void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet);
|
||||
|
||||
// Clear Message Line Cache from Message Renderer
|
||||
void clearMessageCache();
|
||||
|
||||
void scrollUp();
|
||||
void scrollDown();
|
||||
|
||||
} // namespace MessageRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -46,69 +46,99 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
|
||||
}
|
||||
|
||||
// Static variables for dynamic cycling
|
||||
static NodeListMode currentMode = MODE_LAST_HEARD;
|
||||
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
|
||||
static ListMode_Location currentMode_Location = MODE_DISTANCE;
|
||||
static int scrollIndex = 0;
|
||||
// Popup overlay state
|
||||
static uint32_t popupTime = 0;
|
||||
static int popupTotal = 0;
|
||||
static int popupStart = 0;
|
||||
static int popupEnd = 0;
|
||||
static int popupPage = 1;
|
||||
static int popupMaxPage = 1;
|
||||
|
||||
static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible
|
||||
|
||||
// =============================
|
||||
// Scrolling Logic
|
||||
// =============================
|
||||
void scrollUp()
|
||||
{
|
||||
if (scrollIndex > 0)
|
||||
scrollIndex--;
|
||||
|
||||
popupTime = millis(); // show popup
|
||||
}
|
||||
|
||||
void scrollDown()
|
||||
{
|
||||
scrollIndex++;
|
||||
popupTime = millis();
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Utility Functions
|
||||
// =============================
|
||||
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
|
||||
{
|
||||
const char *name = NULL;
|
||||
static char nodeName[16] = "?";
|
||||
if (config.display.use_long_node_name == true) {
|
||||
if (node->has_user && strlen(node->user.long_name) > 0) {
|
||||
name = node->user.long_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
} else {
|
||||
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||
name = node->user.short_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
static char nodeName[25]; // single static buffer we return
|
||||
nodeName[0] = '\0';
|
||||
|
||||
auto writeFallbackId = [&] {
|
||||
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
|
||||
};
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
|
||||
// Use sanitizeString() function and copy directly into nodeName
|
||||
std::string sanitized_name = sanitizeString(name ? name : "");
|
||||
// 2) Sanitize (empty if raw is null/empty)
|
||||
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
|
||||
|
||||
if (!sanitized_name.empty()) {
|
||||
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
|
||||
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
|
||||
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
|
||||
writeFallbackId();
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
// %.*s ensures null-termination and safe truncation to buffer size - 1
|
||||
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
|
||||
}
|
||||
|
||||
if (config.display.use_long_node_name == true) {
|
||||
int availWidth = (SCREEN_WIDTH / 2) - 65;
|
||||
// 4) Width-based truncation + ellipsis (long-name mode only)
|
||||
if (config.display.use_long_node_name && display) {
|
||||
int availWidth = columnWidth - (isHighResolution ? 65 : 38);
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
size_t origLen = strlen(nodeName);
|
||||
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[strlen(nodeName) - 1] = '\0';
|
||||
const size_t beforeLen = std::strlen(nodeName);
|
||||
|
||||
// Trim from the end until it fits or is empty
|
||||
size_t len = beforeLen;
|
||||
while (len && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[--len] = '\0';
|
||||
}
|
||||
|
||||
// If we actually truncated, append "..." (ensure space remains in buffer)
|
||||
if (strlen(nodeName) < origLen) {
|
||||
size_t len = strlen(nodeName);
|
||||
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
|
||||
if (len > maxLen) {
|
||||
nodeName[maxLen] = '\0';
|
||||
len = maxLen;
|
||||
// If truncated, append "..." (respect buffer size)
|
||||
if (len < beforeLen) {
|
||||
// Make sure there's room for "..." and '\0'
|
||||
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
|
||||
const size_t needed = 3; // "..."
|
||||
if (len > capForText - needed) {
|
||||
len = capForText - needed;
|
||||
nodeName[len] = '\0';
|
||||
}
|
||||
strcat(nodeName, "...");
|
||||
std::strcat(nodeName, "...");
|
||||
}
|
||||
}
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle(int screenWidth)
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
switch (currentMode_Nodes) {
|
||||
case MODE_LAST_HEARD:
|
||||
return "Last Heard";
|
||||
case MODE_HOP_SIGNAL:
|
||||
@@ -117,8 +147,18 @@ const char *getCurrentModeTitle(int screenWidth)
|
||||
#else
|
||||
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
|
||||
#endif
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle_Location(int screenWidth)
|
||||
{
|
||||
switch (currentMode_Location) {
|
||||
case MODE_DISTANCE:
|
||||
return "Distance";
|
||||
case MODE_BEARING:
|
||||
return "Bearings";
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
@@ -137,10 +177,8 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
{
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
int separatorX = x + columnWidth - 2;
|
||||
for (int y = yStart; y <= yEnd; y += 2) {
|
||||
display->setPixel(separatorX, y);
|
||||
display->setPixel(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +190,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
|
||||
int perPage = visibleNodeRows * columns;
|
||||
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
@@ -169,7 +208,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -214,7 +253,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -258,7 +297,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -329,18 +368,15 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
switch (currentMode_Nodes) {
|
||||
case MODE_LAST_HEARD:
|
||||
drawEntryLastHeard(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_HOP_SIGNAL:
|
||||
drawEntryHopSignal(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_DISTANCE:
|
||||
drawNodeDistance(display, node, x, y, columnWidth);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -353,7 +389,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -431,11 +467,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
locationScreen = true;
|
||||
else if (strcmp(title, "Distance") == 0)
|
||||
locationScreen = true;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int columnWidth = display->getWidth();
|
||||
#else
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
#endif
|
||||
display->clear();
|
||||
|
||||
// Draw the battery/time header
|
||||
@@ -444,39 +475,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// Space below header
|
||||
y += COMMON_HEADER_HEIGHT;
|
||||
|
||||
int totalColumns = 1; // Default to 1 column
|
||||
|
||||
if (config.display.use_long_node_name) {
|
||||
if (SCREEN_WIDTH <= 240) {
|
||||
totalColumns = 1;
|
||||
} else if (SCREEN_WIDTH > 240) {
|
||||
totalColumns = 2;
|
||||
}
|
||||
} else {
|
||||
if (SCREEN_WIDTH <= 64) {
|
||||
totalColumns = 1;
|
||||
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
|
||||
totalColumns = 2;
|
||||
} else {
|
||||
totalColumns = 3;
|
||||
}
|
||||
}
|
||||
|
||||
int columnWidth = display->getWidth() / totalColumns;
|
||||
|
||||
int totalEntries = nodeDB->getNumMeshNodes();
|
||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
int numskipped = 0;
|
||||
int visibleNodeRows = totalRowsAvailable;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int totalColumns = 1;
|
||||
#else
|
||||
int totalColumns = 2;
|
||||
#endif
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
|
||||
startIndex++; // skip own node
|
||||
}
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
|
||||
// Build filtered + ordered list
|
||||
std::vector<int> drawList;
|
||||
drawList.reserve(totalEntries);
|
||||
for (int i = 0; i < totalEntries; i++) {
|
||||
auto *n = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
if (!n)
|
||||
continue;
|
||||
if (n->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
if (locationScreen && !n->has_position)
|
||||
continue;
|
||||
|
||||
drawList.push_back(n->num);
|
||||
}
|
||||
totalEntries = drawList.size();
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
int maxScroll = 0;
|
||||
if (perPage > 0) {
|
||||
maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
}
|
||||
|
||||
if (scrollIndex > maxScroll)
|
||||
scrollIndex = maxScroll;
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
int shownCount = 0;
|
||||
int rowCount = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i) {
|
||||
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
|
||||
numskipped++;
|
||||
continue;
|
||||
}
|
||||
for (int idx = startIndex; idx < endIndex; idx++) {
|
||||
uint32_t nodeNum = drawList[idx];
|
||||
auto *node = nodeDB->getMeshNode(nodeNum);
|
||||
int xPos = x + (col * columnWidth);
|
||||
int yPos = y + yOffset;
|
||||
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
|
||||
|
||||
if (extras) {
|
||||
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
|
||||
}
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -499,13 +565,71 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// Draw column separator
|
||||
if (shownCount > 0) {
|
||||
const int firstNodeY = y + 3;
|
||||
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) {
|
||||
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
const int scrollStartY = y + 3;
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
|
||||
// Scroll Popup Overlay
|
||||
if (millis() - popupTime < POPUP_DURATION_MS) {
|
||||
popupTotal = totalEntries;
|
||||
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
popupStart = startIndex + 1;
|
||||
popupEnd = std::min(startIndex + perPage, totalEntries);
|
||||
|
||||
popupPage = (scrollIndex + 1);
|
||||
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// Box padding
|
||||
int padding = 2;
|
||||
int textW = display->getStringWidth(buf);
|
||||
int textH = FONT_HEIGHT_SMALL;
|
||||
int boxWidth = textW + padding * 3;
|
||||
int boxHeight = textH + padding * 2;
|
||||
|
||||
// Center of usable screen area:
|
||||
int headerHeight = FONT_HEIGHT_SMALL - 1;
|
||||
int footerHeight = FONT_HEIGHT_SMALL + 2;
|
||||
|
||||
int usableTop = headerHeight;
|
||||
int usableBottom = display->getHeight() - footerHeight;
|
||||
int usableHeight = usableBottom - usableTop;
|
||||
|
||||
// Center point inside usable area
|
||||
int boxLeft = (display->getWidth() - boxWidth) / 2;
|
||||
int boxTop = usableTop + (usableHeight - boxHeight) / 2;
|
||||
|
||||
// Draw Box
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Text
|
||||
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================
|
||||
@@ -513,10 +637,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// =============================
|
||||
|
||||
#ifndef USE_EINK
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
// Node list for Last Heard and Hop Signal views
|
||||
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static NodeListMode lastRenderedMode = MODE_COUNT;
|
||||
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
@@ -529,23 +654,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT) {
|
||||
currentMode = MODE_LAST_HEARD;
|
||||
if (lastRenderedMode == MODE_COUNT_NODE) {
|
||||
currentMode_Nodes = MODE_LAST_HEARD;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
|
||||
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
|
||||
const char *title = getCurrentModeTitle_Nodes(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode;
|
||||
lastRenderedMode = currentMode_Nodes;
|
||||
}
|
||||
|
||||
// Node list for Distance and Bearings views
|
||||
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
if (now - lastSwitchTime >= 3000) {
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT_LOCATION) {
|
||||
currentMode_Location = MODE_DISTANCE;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle_Location(display->getWidth());
|
||||
|
||||
// Render screen based on currentMode_Location
|
||||
if (currentMode_Location == MODE_DISTANCE) {
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
} else if (currentMode_Location == MODE_BEARING) {
|
||||
drawNodeListWithCompasses(display, state, x, y);
|
||||
}
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode_Location;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -566,14 +733,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
||||
#endif
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||
}
|
||||
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Distance";
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
}
|
||||
#endif
|
||||
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
|
||||
@@ -23,8 +23,11 @@ namespace NodeListRenderer
|
||||
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||
|
||||
// Node list mode enumeration
|
||||
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
||||
// Node list mode enumeration for Last Heard and Hop Signal views
|
||||
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 };
|
||||
|
||||
// Node list mode enumeration for Distance and Bearings views
|
||||
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
@@ -35,7 +38,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
@@ -46,14 +49,20 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Utility functions
|
||||
const char *getCurrentModeTitle(int screenWidth);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth);
|
||||
const char *getCurrentModeTitle_Location(int screenWidth);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
|
||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||
|
||||
// Scrolling controls
|
||||
void scrollUp();
|
||||
void scrollDown();
|
||||
|
||||
// Bitmap drawing function
|
||||
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#if HAS_SCREEN
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NotificationRenderer.h"
|
||||
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
int bannerSignalBars = -1;
|
||||
InputEvent NotificationRenderer::inEvent;
|
||||
int8_t NotificationRenderer::curSelected = 0;
|
||||
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||
@@ -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) {
|
||||
@@ -464,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
bool is_picker = false;
|
||||
uint16_t lineCount = 0;
|
||||
// === Layout Configuration ===
|
||||
// Layout Configuration
|
||||
constexpr uint16_t hPadding = 5;
|
||||
constexpr uint16_t vPadding = 2;
|
||||
bool needs_bell = false;
|
||||
@@ -478,13 +491,32 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// Track widest line INCLUDING bars (but don't change per-line widths)
|
||||
uint16_t widestLineWithBars = 0;
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
|
||||
else // if the newline wasn't found, then pull string length from strlen
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
|
||||
// Consider extra width for signal bars on lines that contain "Signal:"
|
||||
uint16_t potentialWidth = lineWidths[lineCount];
|
||||
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
const int gap = 6; // space between text and bars
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
potentialWidth += barsWidth;
|
||||
}
|
||||
|
||||
if (potentialWidth > widestLineWithBars)
|
||||
widestLineWithBars = potentialWidth;
|
||||
|
||||
if (!is_picker) {
|
||||
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
@@ -494,136 +526,12 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
}
|
||||
// count lines
|
||||
|
||||
// Ensure box accounts for signal bars if present
|
||||
if (widestLineWithBars > maxWidth)
|
||||
maxWidth = widestLineWithBars;
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
if (!isHighResolution && boxWidth <= 100)
|
||||
boxWidth += 20;
|
||||
}
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1)
|
||||
boxHeight += (isHighResolution ? 4 : 3);
|
||||
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines)
|
||||
boxWidth += (isHighResolution ? 4 : 2);
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
if (visibleTotalLines == 1) {
|
||||
boxTop += 25;
|
||||
}
|
||||
if (alertBannerOptions < 3) {
|
||||
int missingLines = 3 - alertBannerOptions;
|
||||
int moveUp = missingLines * (effectiveLineHeight / 2);
|
||||
boxTop -= moveUp;
|
||||
if (boxTop < 0)
|
||||
boxTop = 0;
|
||||
}
|
||||
|
||||
// === Draw Box ===
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
int swingRange = 8;
|
||||
static int swingOffset = 0;
|
||||
static bool swingRight = true;
|
||||
static unsigned long lastSwingTime = 0;
|
||||
unsigned long now = millis();
|
||||
int swingSpeedMs = 10 / (swingRange * 2);
|
||||
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
|
||||
lastSwingTime = now;
|
||||
if (swingRight) {
|
||||
swingOffset++;
|
||||
if (swingOffset >= swingRange)
|
||||
swingRight = false;
|
||||
} else {
|
||||
swingOffset--;
|
||||
if (swingOffset <= 0)
|
||||
swingRight = true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
bool isTitle = (i == 0);
|
||||
int globalOptionIndex = (i - 1) + firstOptionToShow;
|
||||
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
|
||||
|
||||
uint16_t visibleWidth = 64 - hPadding * 2;
|
||||
if (totalLines > visibleTotalLines)
|
||||
visibleWidth -= 6;
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
|
||||
if (isTitle) {
|
||||
if (visibleTotalLines == 1) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
display->setColor(WHITE);
|
||||
if (needs_bell) {
|
||||
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
|
||||
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
}
|
||||
lineY = boxTop + effectiveLineHeight + 1;
|
||||
} else if (isSelectedOption) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
|
||||
int textX = boxLeft + hPadding + swingOffset;
|
||||
display->drawString(textX, lineY - 1, lineBuffer);
|
||||
} else {
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += effectiveLineHeight;
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
|
||||
lineY += effectiveLineHeight;
|
||||
}
|
||||
}
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#else
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
@@ -645,8 +553,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
boxWidth += (isHighResolution) ? 4 : 2;
|
||||
}
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
boxHeight += (isHighResolution) ? 2 : 1;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (visibleTotalLines == 1) {
|
||||
boxTop += 25;
|
||||
}
|
||||
if (alertBannerOptions < 3) {
|
||||
int missingLines = 3 - alertBannerOptions;
|
||||
int moveUp = missingLines * (effectiveLineHeight / 2);
|
||||
boxTop -= moveUp;
|
||||
if (boxTop < 0)
|
||||
boxTop = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// === Draw Box ===
|
||||
// Draw Box
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
@@ -662,7 +583,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// === Draw Content ===
|
||||
// Draw Content
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
@@ -691,17 +612,47 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Pop-up
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
// If this is the Signal line, center text + bars as one group
|
||||
bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr);
|
||||
if (isSignalLine) {
|
||||
const int totalBars = 5;
|
||||
const int barWidth = 3;
|
||||
const int barSpacing = 2;
|
||||
const int barHeightStep = 2;
|
||||
const int gap = 6;
|
||||
|
||||
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
int totalWidth = textWidth + barsWidth;
|
||||
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
|
||||
|
||||
display->drawString(groupStartX, lineY, lineBuffer);
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
int baseY = lineY + effectiveLineHeight - 1;
|
||||
for (int b = 0; b < totalBars; b++) {
|
||||
int barHeight = (b + 1) * barHeightStep;
|
||||
int x = baseX + b * (barWidth + barSpacing);
|
||||
int y = baseY - barHeight;
|
||||
|
||||
if (b < graphics::bannerSignalBars) {
|
||||
display->fillRect(x, y, barWidth, barHeight);
|
||||
} else {
|
||||
display->drawRect(x, y, barWidth, barHeight);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
lineY += (effectiveLineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// === Scroll Bar (Thicker, inside box, not over title) ===
|
||||
// Scroll Bar (Thicker, inside box, not over title)
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
@@ -712,7 +663,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Draw the last text message we received
|
||||
@@ -769,40 +719,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 +739,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 +773,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
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#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);
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
#include "NodeListRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/images.h"
|
||||
@@ -29,6 +26,16 @@ namespace graphics
|
||||
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
||||
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
|
||||
|
||||
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
int yOffset = (isHighResolution) ? -5 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite);
|
||||
}
|
||||
}
|
||||
|
||||
void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||
{
|
||||
favoritedNodes.clear();
|
||||
@@ -244,16 +251,16 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
|
||||
|
||||
// Draw nodes status
|
||||
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
|
||||
bool show_total, String additional_words)
|
||||
bool show_total, const char *additional_words)
|
||||
{
|
||||
char usersString[20];
|
||||
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0;
|
||||
|
||||
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str());
|
||||
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words);
|
||||
|
||||
if (show_total) {
|
||||
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0;
|
||||
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str());
|
||||
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words);
|
||||
}
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
@@ -600,14 +607,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
|
||||
} else {
|
||||
@@ -759,7 +759,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
|
||||
// Start Functions to write date/time to the screen
|
||||
// Helper function to check if a year is a leap year
|
||||
bool isLeapYear(int year)
|
||||
constexpr bool isLeapYear(int year)
|
||||
{
|
||||
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
||||
}
|
||||
@@ -990,14 +990,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
} else {
|
||||
|
||||
@@ -34,7 +34,7 @@ class UIRenderer
|
||||
public:
|
||||
// Common UI elements
|
||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||
int node_offset = 0, bool show_total = true, String additional_words = "");
|
||||
int node_offset = 0, bool show_total = true, const char *additional_words = "");
|
||||
|
||||
// GPS status functions
|
||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
@@ -43,9 +43,6 @@ class UIRenderer
|
||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
|
||||
// Layout and utility functions
|
||||
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
|
||||
|
||||
// Overlay and special screens
|
||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||
|
||||
@@ -83,8 +80,6 @@ class UIRenderer
|
||||
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||
|
||||
// Message filtering
|
||||
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
|
||||
// Check if the display can render a string (detect special chars; emoji)
|
||||
static bool haveGlyphs(const char *str);
|
||||
}; // namespace UIRenderer
|
||||
|
||||
@@ -304,58 +304,6 @@ const uint8_t chirpy[] = {
|
||||
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
|
||||
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||
|
||||
#define chirpy_width_hirez 76
|
||||
#define chirpy_height_hirez 100
|
||||
const uint8_t chirpy_hirez[] = {
|
||||
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
|
||||
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
|
||||
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
|
||||
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
|
||||
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
|
||||
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
|
||||
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
|
||||
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
|
||||
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
|
||||
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
|
||||
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
|
||||
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
|
||||
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
|
||||
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
|
||||
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
|
||||
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
|
||||
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
|
||||
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
|
||||
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
|
||||
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
|
||||
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
|
||||
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
|
||||
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
|
||||
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
|
||||
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
|
||||
|
||||
#define chirpy_small_image_width 8
|
||||
#define chirpy_small_image_height 8
|
||||
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
|
||||
@@ -88,6 +88,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 +157,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
|
||||
|
||||
@@ -49,10 +49,14 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, 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;
|
||||
|
||||
@@ -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<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
@@ -40,8 +48,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, 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;
|
||||
|
||||
@@ -489,8 +489,6 @@ int32_t KbI2cBase::runOnce()
|
||||
case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT
|
||||
case 0x91: // fn+t
|
||||
case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE
|
||||
|
||||
case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME
|
||||
case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE
|
||||
case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST
|
||||
// just pass those unmodified
|
||||
|
||||
@@ -439,6 +439,11 @@ void setup()
|
||||
|
||||
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
|
||||
|
||||
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||||
// use PSRAM for malloc calls > 256 bytes
|
||||
heap_caps_malloc_extmem_enable(256);
|
||||
#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));
|
||||
|
||||
@@ -44,6 +44,7 @@ struct UIFrameEvent {
|
||||
REDRAW_ONLY, // Don't change which frames are show, just redraw, asap
|
||||
REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus()
|
||||
REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout
|
||||
SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen
|
||||
} action = REDRAW_ONLY;
|
||||
|
||||
// We might want to pass additional data inside this struct at some point
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
#include "../concurrency/Periodic.h"
|
||||
#include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
@@ -192,8 +194,16 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
p.id = generatePacketId(); // If the phone didn't supply one, then pick one
|
||||
|
||||
p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone
|
||||
// (so we update our nodedb for the local node)
|
||||
|
||||
#if HAS_SCREEN
|
||||
if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && p.to != NODENUM_BROADCAST &&
|
||||
p.to != 0) // DM only
|
||||
{
|
||||
perhapsDecode(&p);
|
||||
const StoredMessage &sm = messageStore.addFromPacket(p);
|
||||
graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI
|
||||
}
|
||||
#endif
|
||||
// Send the packet into the mesh
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
|
||||
@@ -805,11 +805,15 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.external_notification.output_ms = 500;
|
||||
moduleConfig.external_notification.nag_timeout = 2;
|
||||
#endif
|
||||
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312)
|
||||
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE)
|
||||
// Default to RAK led pin 2 (blue)
|
||||
moduleConfig.external_notification.enabled = true;
|
||||
moduleConfig.external_notification.output = PIN_LED2;
|
||||
#if defined(MUZI_BASE)
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
|
||||
Allocator<meshtastic_MeshPacket> &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 + \
|
||||
|
||||
@@ -237,8 +237,8 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_T_ETH_ELITE = 91,
|
||||
/* Heltec HRI-3621 industrial probe */
|
||||
meshtastic_HardwareModel_HELTEC_SENSOR_HUB = 92,
|
||||
/* Reserved Fried Chicken ID for future use */
|
||||
meshtastic_HardwareModel_RESERVED_FRIED_CHICKEN = 93,
|
||||
/* Muzi Works Muzi-Base device */
|
||||
meshtastic_HardwareModel_MUZI_BASE = 93,
|
||||
/* Heltec Magnetic Power Bank with Meshtastic compatible */
|
||||
meshtastic_HardwareModel_HELTEC_MESH_POCKET = 94,
|
||||
/* Seeed Solar Node */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,7 +75,6 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
// === State/UI ===
|
||||
bool shouldDraw();
|
||||
bool hasMessages();
|
||||
void showTemporaryMessage(const String &message);
|
||||
void resetSearch();
|
||||
void updateDestinationSelectionList();
|
||||
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
@@ -153,10 +152,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
String searchQuery;
|
||||
String freetext;
|
||||
String temporaryMessage;
|
||||
|
||||
// === Message Storage ===
|
||||
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
|
||||
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
|
||||
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
|
||||
int messagesCount = 0;
|
||||
int currentMessageIndex = -1;
|
||||
@@ -167,14 +165,11 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
|
||||
ChannelIndex channel = 0; // Channel index used when sending a message
|
||||
|
||||
bool ack = false; // True = ACK received, False = NACK or failed
|
||||
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
||||
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
|
||||
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
|
||||
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
|
||||
|
||||
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
||||
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
||||
bool ack = false; // True = ACK received, False = NACK or failed
|
||||
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
||||
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
||||
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
||||
uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet
|
||||
|
||||
// === State Tracking ===
|
||||
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
@@ -60,9 +60,7 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
|
||||
graphics::decomposeTime(rtc_sec, hour, min, sec);
|
||||
}
|
||||
|
||||
// Check if the dropzone is open or closed by reading the analog pin
|
||||
|
||||
@@ -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 {
|
||||
@@ -319,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
|
||||
|
||||
@@ -541,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
272
src/modules/OnScreenKeyboardModule.cpp
Normal file
272
src/modules/OnScreenKeyboardModule.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <Arduino.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
OnScreenKeyboardModule &OnScreenKeyboardModule::instance()
|
||||
{
|
||||
static OnScreenKeyboardModule inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
OnScreenKeyboardModule::~OnScreenKeyboardModule()
|
||||
{
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> cb)
|
||||
{
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
keyboard = new VirtualKeyboard();
|
||||
callback = cb;
|
||||
if (header)
|
||||
keyboard->setHeader(header);
|
||||
if (initialText)
|
||||
keyboard->setInputText(initialText);
|
||||
|
||||
// Route VK submission/cancel events back into the module
|
||||
keyboard->setCallback([this](const std::string &text) {
|
||||
if (text.empty()) {
|
||||
this->onCancel();
|
||||
} else {
|
||||
this->onSubmit(text);
|
||||
}
|
||||
});
|
||||
|
||||
// Maintain legacy compatibility hooks
|
||||
NotificationRenderer::virtualKeyboard = keyboard;
|
||||
NotificationRenderer::textInputCallback = callback;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::stop(bool callEmptyCallback)
|
||||
{
|
||||
auto cb = callback;
|
||||
callback = nullptr;
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
// Keep NotificationRenderer legacy pointers in sync
|
||||
NotificationRenderer::virtualKeyboard = nullptr;
|
||||
NotificationRenderer::textInputCallback = nullptr;
|
||||
clearPopup();
|
||||
if (callEmptyCallback && cb)
|
||||
cb("");
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::handleInput(const InputEvent &event)
|
||||
{
|
||||
if (!keyboard)
|
||||
return;
|
||||
|
||||
if (processVirtualKeyboardInput(event, keyboard))
|
||||
return;
|
||||
|
||||
if (event.inputEvent == INPUT_BROKER_CANCEL)
|
||||
onCancel();
|
||||
}
|
||||
|
||||
bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard)
|
||||
{
|
||||
if (!targetKeyboard)
|
||||
return false;
|
||||
|
||||
switch (event.inputEvent) {
|
||||
case INPUT_BROKER_UP:
|
||||
case INPUT_BROKER_UP_LONG:
|
||||
targetKeyboard->moveCursorUp();
|
||||
return true;
|
||||
case INPUT_BROKER_DOWN:
|
||||
case INPUT_BROKER_DOWN_LONG:
|
||||
targetKeyboard->moveCursorDown();
|
||||
return true;
|
||||
case INPUT_BROKER_LEFT:
|
||||
case INPUT_BROKER_ALT_PRESS:
|
||||
targetKeyboard->moveCursorLeft();
|
||||
return true;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
targetKeyboard->moveCursorRight();
|
||||
return true;
|
||||
case INPUT_BROKER_SELECT:
|
||||
targetKeyboard->handlePress();
|
||||
return true;
|
||||
case INPUT_BROKER_SELECT_LONG:
|
||||
targetKeyboard->handleLongPress();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
|
||||
{
|
||||
if (!keyboard)
|
||||
return false;
|
||||
|
||||
// Timeout
|
||||
if (keyboard->isTimedOut()) {
|
||||
onCancel();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear full screen behind keyboard
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||
display->setColor(WHITE);
|
||||
keyboard->draw(display, 0, 0);
|
||||
|
||||
// Draw popup overlay if needed
|
||||
drawPopup(display);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::onSubmit(const std::string &text)
|
||||
{
|
||||
auto cb = callback;
|
||||
stop(false);
|
||||
if (cb)
|
||||
cb(text);
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::onCancel()
|
||||
{
|
||||
stop(true);
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
|
||||
{
|
||||
if (!title || !content)
|
||||
return;
|
||||
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
|
||||
popupTitle[sizeof(popupTitle) - 1] = '\0';
|
||||
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
|
||||
popupMessage[sizeof(popupMessage) - 1] = '\0';
|
||||
popupUntil = millis() + durationMs;
|
||||
popupVisible = true;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::clearPopup()
|
||||
{
|
||||
popupTitle[0] = '\0';
|
||||
popupMessage[0] = '\0';
|
||||
popupUntil = 0;
|
||||
popupVisible = false;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display)
|
||||
{
|
||||
// Only render the popup overlay (without drawing the keyboard)
|
||||
drawPopup(display);
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
|
||||
{
|
||||
if (!popupVisible)
|
||||
return;
|
||||
if (millis() > popupUntil || popupMessage[0] == '\0') {
|
||||
popupVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
|
||||
constexpr uint16_t maxContentLines = 3;
|
||||
const bool hasTitle = popupTitle[0] != '\0';
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
const uint16_t maxWrapWidth = display->width() - 40;
|
||||
|
||||
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
|
||||
std::vector<std::string> wrapped;
|
||||
std::string current;
|
||||
std::string word;
|
||||
const char *p = text;
|
||||
while (*p && wrapped.size() < maxContentLines) {
|
||||
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
|
||||
if (*p == '\n') {
|
||||
if (!current.empty()) {
|
||||
wrapped.push_back(current);
|
||||
current.clear();
|
||||
if (wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
}
|
||||
}
|
||||
++p;
|
||||
}
|
||||
if (!*p || wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
word.clear();
|
||||
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
|
||||
word += *p++;
|
||||
if (word.empty())
|
||||
continue;
|
||||
std::string test = current.empty() ? word : (current + " " + word);
|
||||
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
|
||||
if (w <= availableWidth)
|
||||
current = test;
|
||||
else {
|
||||
if (!current.empty()) {
|
||||
wrapped.push_back(current);
|
||||
current = word;
|
||||
if (wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
} else {
|
||||
current = word;
|
||||
while (current.size() > 1 &&
|
||||
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
|
||||
current.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!current.empty() && wrapped.size() < maxContentLines)
|
||||
wrapped.push_back(current);
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
std::vector<std::string> allLines;
|
||||
if (hasTitle)
|
||||
allLines.emplace_back(popupTitle);
|
||||
|
||||
char buf[sizeof(popupMessage)];
|
||||
strncpy(buf, popupMessage, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
char *paragraph = strtok(buf, "\n");
|
||||
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
|
||||
auto wrapped = wrapText(paragraph, maxWrapWidth);
|
||||
for (const auto &ln : wrapped) {
|
||||
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
|
||||
break;
|
||||
allLines.push_back(ln);
|
||||
}
|
||||
paragraph = strtok(nullptr, "\n");
|
||||
}
|
||||
|
||||
std::vector<const char *> ptrs;
|
||||
for (const auto &ln : allLines)
|
||||
ptrs.push_back(ln.c_str());
|
||||
ptrs.push_back(nullptr);
|
||||
|
||||
// Use the standard notification box drawing from NotificationRenderer
|
||||
NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
55
src/modules/OnScreenKeyboardModule.h
Normal file
55
src/modules/OnScreenKeyboardModule.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "graphics/Screen.h" // InputEvent
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
class OnScreenKeyboardModule
|
||||
{
|
||||
public:
|
||||
static OnScreenKeyboardModule &instance();
|
||||
|
||||
void start(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> callback);
|
||||
|
||||
void stop(bool callEmptyCallback);
|
||||
|
||||
void handleInput(const InputEvent &event);
|
||||
static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard);
|
||||
bool draw(OLEDDisplay *display);
|
||||
|
||||
void showPopup(const char *title, const char *content, uint32_t durationMs);
|
||||
void clearPopup();
|
||||
// Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard)
|
||||
void drawPopupOverlay(OLEDDisplay *display);
|
||||
|
||||
private:
|
||||
OnScreenKeyboardModule() = default;
|
||||
~OnScreenKeyboardModule();
|
||||
OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete;
|
||||
OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete;
|
||||
|
||||
void onSubmit(const std::string &text);
|
||||
void onCancel();
|
||||
|
||||
void drawPopup(OLEDDisplay *display);
|
||||
|
||||
VirtualKeyboard *keyboard = nullptr;
|
||||
std::function<void(const std::string &)> callback;
|
||||
|
||||
char popupTitle[64] = {0};
|
||||
char popupMessage[256] = {0};
|
||||
uint32_t popupUntil = 0;
|
||||
bool popupVisible = false;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
@@ -45,8 +45,12 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
{
|
||||
auto p = *pptr;
|
||||
|
||||
// If inbound message is a replay (or spoof!) of our own messages, we shouldn't process
|
||||
// (why use second-hand sources for our own data?)
|
||||
const auto transport = mp.transport_mechanism;
|
||||
if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL,
|
||||
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) {
|
||||
LOG_WARN("Ignoring packet supposedly from us over external transport");
|
||||
return true;
|
||||
}
|
||||
|
||||
// FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER)
|
||||
// to set fixed location, EUD-GPS location or just the time (see also issue #900)
|
||||
@@ -472,19 +476,53 @@ void PositionModule::sendLostAndFoundText()
|
||||
delete[] message;
|
||||
}
|
||||
|
||||
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
|
||||
static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon)
|
||||
{
|
||||
if (precisionBits > 0 && precisionBits < 32) {
|
||||
// Build mask for top 'precisionBits' bits of a 32-bit unsigned field
|
||||
const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits));
|
||||
// Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but
|
||||
// the bitmask logic used previously operated as unsigned—preserve that behavior by
|
||||
// casting to uint32_t for masking, then back to int32_t.
|
||||
uint32_t lat_u = static_cast<uint32_t>(inLat) & mask;
|
||||
uint32_t lon_u = static_cast<uint32_t>(inLon) & mask;
|
||||
|
||||
// Add the "center of cell" offset used elsewhere:
|
||||
// The code previously added (1 << (31 - precision)) to produce the middle of the possible location.
|
||||
uint32_t center_offset = (1u << (31 - precisionBits));
|
||||
lat_u += center_offset;
|
||||
lon_u += center_offset;
|
||||
|
||||
outLat = static_cast<int32_t>(lat_u);
|
||||
outLon = static_cast<int32_t>(lon_u);
|
||||
} else {
|
||||
// full precision: return input unchanged
|
||||
outLat = inLat;
|
||||
outLon = inLon;
|
||||
}
|
||||
}
|
||||
|
||||
struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition)
|
||||
{
|
||||
// The minimum distance to travel before we are able to send a new position packet.
|
||||
const uint32_t distanceTravelThreshold =
|
||||
Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100);
|
||||
|
||||
// Determine the distance in meters between two points on the globe
|
||||
float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter(
|
||||
lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7);
|
||||
int32_t lastLatImprecise, lastLonImprecise;
|
||||
int32_t currentLatImprecise, currentLonImprecise;
|
||||
|
||||
return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend),
|
||||
computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise);
|
||||
computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise,
|
||||
currentLonImprecise);
|
||||
|
||||
float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7,
|
||||
currentLonImprecise * 1e-7);
|
||||
|
||||
float distanceTraveled = fabsf(distMeters);
|
||||
|
||||
return SmartPosition{.distanceTraveled = distanceTraveled,
|
||||
.distanceThreshold = distanceTravelThreshold,
|
||||
.hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold};
|
||||
.hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold};
|
||||
}
|
||||
|
||||
void PositionModule::handleNewPosition()
|
||||
|
||||
@@ -75,6 +75,12 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit
|
||||
return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
|
||||
uint8_t hopLimit)
|
||||
{
|
||||
return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit);
|
||||
}
|
||||
|
||||
RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg)
|
||||
{
|
||||
isPromiscuous = true;
|
||||
|
||||
@@ -16,6 +16,9 @@ class RoutingModule : public ProtobufModule<meshtastic_Routing>
|
||||
virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
||||
bool ackWantsAck = false);
|
||||
|
||||
meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
|
||||
uint8_t hopLimit = 0);
|
||||
|
||||
// Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response
|
||||
uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "SystemCommandsModule.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
#include "MessageStore.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#endif
|
||||
|
||||
#include "GPS.h"
|
||||
#include "MeshService.h"
|
||||
#include "Module.h"
|
||||
@@ -28,10 +31,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
switch (event->kbchar) {
|
||||
// Fn key symbols
|
||||
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
|
||||
IF_SCREEN(screen->setFunctionSymbol("Fn"));
|
||||
return 0;
|
||||
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
|
||||
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
|
||||
return 0;
|
||||
// Brightness
|
||||
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
|
||||
@@ -78,6 +78,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
case INPUT_BROKER_MSG_REBOOT:
|
||||
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
|
||||
nodeDB->saveToDisk();
|
||||
#if HAS_SCREEN
|
||||
messageStore.saveToFlash();
|
||||
#endif
|
||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
return true;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "TextMessageModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "buzz.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "main.h"
|
||||
TextMessageModule *textMessageModule;
|
||||
|
||||
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
@@ -15,14 +19,26 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
|
||||
#endif
|
||||
|
||||
// We only store/display messages destined for us.
|
||||
// Keep a copy of the most recent text message.
|
||||
devicestate.rx_text_message = mp;
|
||||
devicestate.has_rx_text_message = true;
|
||||
#if HAS_SCREEN
|
||||
// Guard against running in MeshtasticUI
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
// Store in the central message history
|
||||
const StoredMessage &sm = messageStore.addFromPacket(mp);
|
||||
|
||||
// Pass message to renderer (banner + thread switching + scroll reset)
|
||||
// Use the global Screen singleton to retrieve the current OLED display
|
||||
auto *display = screen ? screen->getDisplayDevice() : nullptr;
|
||||
graphics::MessageRenderer::handleNewMessage(display, sm, mp);
|
||||
}
|
||||
#endif
|
||||
// Only trigger screen wake if configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
powerFSM.trigger(EVENT_RECEIVED_MSG);
|
||||
}
|
||||
|
||||
// Notify any observers (e.g. external modules that care about packets)
|
||||
notifyObservers(&mp);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
/**
|
||||
* Text message handling for meshtastic - draws on the OLED display the most recent received message
|
||||
* Text message handling for Meshtastic.
|
||||
*
|
||||
* This module is responsible for receiving and storing incoming text messages
|
||||
* from the mesh. It updates device state and notifies observers so that other
|
||||
* components (such as the MessageRenderer) can later display or process them.
|
||||
*
|
||||
* Rendering of messages on screen is no longer done here.
|
||||
*/
|
||||
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
|
||||
{
|
||||
@@ -15,10 +21,10 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
|
||||
@return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
|
||||
it
|
||||
*/
|
||||
*
|
||||
* @return ProcessMessage::STOP if you've guaranteed you've handled this
|
||||
* message and no other handlers should be considered for it.
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
@@ -14,6 +15,15 @@
|
||||
|
||||
WaypointModule *waypointModule;
|
||||
|
||||
static inline float degToRad(float deg)
|
||||
{
|
||||
return deg * PI / 180.0f;
|
||||
}
|
||||
static inline float radToDeg(float rad)
|
||||
{
|
||||
return rad * 180.0f / PI;
|
||||
}
|
||||
|
||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
@@ -52,31 +62,15 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
bool WaypointModule::shouldDraw()
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
||||
if (screen == nullptr)
|
||||
return false;
|
||||
// If no waypoint to show
|
||||
if (!devicestate.has_rx_waypoint)
|
||||
if (!screen || !devicestate.has_rx_waypoint)
|
||||
return false;
|
||||
|
||||
// Decode the message, to find the expiration time (is waypoint still valid)
|
||||
// This handles "deletion" as well as expiration
|
||||
meshtastic_Waypoint wp;
|
||||
memset(&wp, 0, sizeof(wp));
|
||||
meshtastic_Waypoint wp{}; // <- replaces memset
|
||||
if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
|
||||
&meshtastic_Waypoint_msg, &wp)) {
|
||||
// Valid waypoint
|
||||
if (wp.expire > getTime())
|
||||
return devicestate.has_rx_waypoint = true;
|
||||
|
||||
// Expired, or deleted
|
||||
else
|
||||
return devicestate.has_rx_waypoint = false;
|
||||
return wp.expire > getTime();
|
||||
}
|
||||
|
||||
// If decoding failed
|
||||
LOG_ERROR("Failed to decode waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
return false;
|
||||
return false; // no LOG_ERROR, no flag writes
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -85,53 +79,46 @@ bool WaypointModule::shouldDraw()
|
||||
/// Draw the last waypoint we received
|
||||
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (screen == nullptr)
|
||||
if (!screen)
|
||||
return;
|
||||
// Prepare to draw
|
||||
display->setFont(FONT_SMALL);
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
|
||||
// Handle inverted display
|
||||
// Unsure of expected behavior: for now, copy drawNodeInfo
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
|
||||
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||
// === Set Title
|
||||
const char *titleStr = "Waypoint";
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
const int w = display->getWidth();
|
||||
const int h = display->getHeight();
|
||||
|
||||
// Decode the waypoint
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
||||
meshtastic_Waypoint wp;
|
||||
memset(&wp, 0, sizeof(wp));
|
||||
meshtastic_Waypoint wp{};
|
||||
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) {
|
||||
// This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case
|
||||
display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint");
|
||||
devicestate.has_rx_waypoint = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get timestamp info. Will pass as a field to drawColumns
|
||||
static char lastStr[20];
|
||||
char lastStr[20];
|
||||
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
||||
|
||||
// Will contain distance information, passed as a field to drawColumns
|
||||
static char distStr[20];
|
||||
char distStr[20];
|
||||
|
||||
// Get our node, to use our own position
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// Text fields to draw (left of compass)
|
||||
// Last element must be NULL. This signals the end of the char*[] to drawColumns
|
||||
const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL};
|
||||
|
||||
// Dimensions / co-ordinates for the compass/circle
|
||||
int16_t compassX = 0, compassY = 0;
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
|
||||
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + display->getHeight() / 2;
|
||||
} else {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
|
||||
}
|
||||
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
|
||||
const int16_t compassX = x + w - (compassDiam / 2) - 5;
|
||||
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
|
||||
? y + h / 2
|
||||
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
// If our node has a position:
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||
@@ -141,7 +128,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
myHeading = 0;
|
||||
} else {
|
||||
if (screen->hasHeading())
|
||||
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
|
||||
myHeading = degToRad(screen->getHeading());
|
||||
else
|
||||
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
}
|
||||
@@ -157,46 +144,35 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
|
||||
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
|
||||
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
|
||||
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
|
||||
|
||||
// Distance to Waypoint
|
||||
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
if (d < (2 * MILES_TO_FEET))
|
||||
snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
} else {
|
||||
if (d < 2000)
|
||||
snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees);
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If our node doesn't have position
|
||||
else {
|
||||
// ? in the compass
|
||||
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
|
||||
|
||||
// ? in the distance field
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
|
||||
strncpy(distStr, "? mi ?°", sizeof(distStr));
|
||||
else
|
||||
strncpy(distStr, "? km ?°", sizeof(distStr));
|
||||
snprintf(distStr, sizeof(distStr), "? %s ?°",
|
||||
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
|
||||
}
|
||||
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
|
||||
// Undo color-inversion, if set prior to drawing header
|
||||
// Unsure of expected behavior? For now: copy drawNodeInfo
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
// Must be after distStr is populated
|
||||
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -87,10 +87,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
// Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message.
|
||||
// We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node
|
||||
// receives it when we get our own packet back. Then we'll stop our retransmissions.
|
||||
if (isFromUs(e.packet))
|
||||
routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
|
||||
else
|
||||
if (isFromUs(e.packet)) {
|
||||
auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index);
|
||||
pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
||||
router->sendLocal(pAck);
|
||||
} else {
|
||||
LOG_INFO("Ignore downlink message we originally sent");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isFromUs(e.packet)) {
|
||||
|
||||
@@ -651,8 +651,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
{
|
||||
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
const uint16_t connHandle = connInfo.getConnHandle();
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int phyResult =
|
||||
ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY);
|
||||
if (phyResult == 0) {
|
||||
@@ -660,6 +660,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
} else {
|
||||
LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult);
|
||||
}
|
||||
#endif
|
||||
|
||||
int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult == 0) {
|
||||
@@ -670,9 +671,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
|
||||
LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu);
|
||||
pServer->updateConnParams(connHandle, 6, 12, 0, 200);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
|
||||
{
|
||||
LOG_INFO("BLE disconnect reason: %d", reason);
|
||||
@@ -818,7 +820,7 @@ void NimbleBluetooth::setup()
|
||||
NimBLEDevice::init(getDeviceName());
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu);
|
||||
if (mtuResult == 0) {
|
||||
LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu);
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
#elif defined(HELTEC_MESH_SOLAR)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR
|
||||
#elif defined(MUZI_BASE)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_RESERVED_FRIED_CHICKEN
|
||||
#define HW_VENDOR meshtastic_HardwareModel_MUZI_BASE
|
||||
#else
|
||||
#define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN
|
||||
#endif
|
||||
|
||||
@@ -335,6 +335,16 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
if (Serial1) // A straightforward solution to the wake from deepsleep problem
|
||||
Serial1.end();
|
||||
#endif
|
||||
|
||||
#ifdef TTGO_T_ECHO
|
||||
// To power off the T-Echo, the display must be set
|
||||
// as an input pin; otherwise, there will be leakage current.
|
||||
pinMode(PIN_EINK_CS, INPUT);
|
||||
pinMode(PIN_EINK_DC, INPUT);
|
||||
pinMode(PIN_EINK_RES, INPUT);
|
||||
pinMode(PIN_EINK_BUSY, INPUT);
|
||||
#endif
|
||||
|
||||
setBluetoothEnable(false);
|
||||
|
||||
#ifdef RAK4630
|
||||
|
||||
@@ -426,11 +426,13 @@ void portduinoSetup()
|
||||
}
|
||||
|
||||
getMacAddr(dmac);
|
||||
#ifndef UNIT_TEST
|
||||
if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) {
|
||||
std::cout << "*** Blank MAC Address not allowed!" << std::endl;
|
||||
std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
#endif
|
||||
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
|
||||
// Rather important to set this, if not running simulated.
|
||||
randomSeed(time(NULL));
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
[esp32_base]
|
||||
extends = arduino_base
|
||||
custom_esp32_kind = esp32
|
||||
custom_mtjson_part =
|
||||
platform =
|
||||
# renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32
|
||||
platformio/espressif32@6.11.0
|
||||
platformio/espressif32@6.12.0
|
||||
|
||||
extra_scripts =
|
||||
${env.extra_scripts}
|
||||
pre:extra_scripts/esp32_pre.py
|
||||
extra_scripts/esp32_extra.py
|
||||
|
||||
build_src_filter =
|
||||
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
||||
|
||||
@@ -13,7 +13,10 @@ build_flags =
|
||||
-DEINK_WIDTH=250
|
||||
-DEINK_HEIGHT=122
|
||||
-DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk
|
||||
-DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted
|
||||
-DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted //20
|
||||
-DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates //30
|
||||
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
|
||||
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
|
||||
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
|
||||
@@ -114,5 +114,5 @@ extends = env:native
|
||||
build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:native.build_flags}
|
||||
; https://docs.platformio.org/en/latest/projectconf/sections/env/options/test/test_testing_command.html
|
||||
test_testing_command =
|
||||
${platformio.build_dir}/${this.__env__}/program
|
||||
${platformio.build_dir}/${this.__env__}/meshtasticd
|
||||
-s
|
||||
|
||||
@@ -62,17 +62,11 @@ extern "C" {
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
#define PIN_BUTTON2 (32 + 10)
|
||||
#define PIN_BUTTON1 (32 + 10)
|
||||
#define PIN_BUTTON2 (32 + 7)
|
||||
#define ALT_BUTTON_PIN PIN_BUTTON2
|
||||
#define ALT_BUTTON_ACTIVE_LOW true
|
||||
#define ALT_BUTTON_ACTIVE_PULLUP true
|
||||
#define PIN_BUTTON1 (32 + 7)
|
||||
|
||||
// #define PIN_BUTTON1 (0 + 11)
|
||||
// #define PIN_BUTTON1 (32 + 7)
|
||||
|
||||
// #define BUTTON_CLICK_MS 400
|
||||
// #define BUTTON_TOUCH_MS 200
|
||||
|
||||
/*
|
||||
* Analog pins
|
||||
|
||||
@@ -46,6 +46,14 @@ build_flags = ${heltec_mesh_solar_base.build_flags}
|
||||
-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
|
||||
-DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
|
||||
-DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting"
|
||||
-DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build
|
||||
-DMESHTASTIC_EXCLUDE_WIFI=1
|
||||
-DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||
-DMESHTASTIC_EXCLUDE_STOREFORWARD=1
|
||||
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||
-DMESHTASTIC_EXCLUDE_WAYPOINT=1
|
||||
lib_deps =
|
||||
${heltec_mesh_solar_base.lib_deps}
|
||||
https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip
|
||||
|
||||
@@ -64,6 +64,7 @@ extern "C" {
|
||||
#define INPUTDRIVER_ENCODER_UP 26
|
||||
#define INPUTDRIVER_ENCODER_DOWN 4
|
||||
#define INPUTDRIVER_ENCODER_BTN 28
|
||||
#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150
|
||||
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 1
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user