Compare commits
137
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84355d15a | ||
|
|
5d12edbe07 | ||
|
|
398d1adea1 | ||
|
|
393cd96ba2 | ||
|
|
6658ec2603 | ||
|
|
b941dab900 | ||
|
|
ffe6f706f9 | ||
|
|
0ed537a336 | ||
|
|
86dad90573 | ||
|
|
20db5fdbb7 | ||
|
|
5b1ea922f3 | ||
|
|
5b94f580dc | ||
|
|
3d524be965 | ||
|
|
4ec59ce75d | ||
|
|
cb28c38828 | ||
|
|
969aefa551 | ||
|
|
3f5828c0e9 | ||
|
|
5cf365245e | ||
|
|
fdd17ac75c | ||
|
|
9e40c8893f | ||
|
|
afd5e29bce | ||
|
|
20f838a61e | ||
|
|
58736f5184 | ||
|
|
22031c5886 | ||
|
|
fe4e75eb66 | ||
|
|
42075da09e | ||
|
|
6ba82ed5b6 | ||
|
|
935b0504d4 | ||
|
|
a8fed3256e | ||
|
|
1626836a71 | ||
|
|
66161da2a7 | ||
|
|
6bbdfe1797 | ||
|
|
7ea28d34b2 | ||
|
|
65bf1630b3 | ||
|
|
3972fe1fd0 | ||
|
|
5ced739a57 | ||
|
|
b7bf251798 | ||
|
|
90742fc87e | ||
|
|
f7f356e2e2 | ||
|
|
1fad7facd6 | ||
|
|
153ab81a9f | ||
|
|
8d73d67246 | ||
|
|
0d46943bb8 | ||
|
|
deb45e471c | ||
|
|
3601eabbf8 | ||
|
|
eb2f3cef89 | ||
|
|
85be5965b0 | ||
|
|
1e34c1ef1b | ||
|
|
5f852a1c05 | ||
|
|
1d90ea6f1d | ||
|
|
65eee22396 | ||
|
|
cb938266c7 | ||
|
|
8093e2ed5a | ||
|
|
80af726877 | ||
|
|
3f5dc78744 | ||
|
|
563adc6aaa | ||
|
|
67f806b0a5 | ||
|
|
c7dc536640 | ||
|
|
6920a5f8b1 | ||
|
|
2b7a230977 | ||
|
|
1e16185fdd | ||
|
|
60c2592694 | ||
|
|
fc0ad4956e | ||
|
|
43aa90fc3f | ||
|
|
c28bdbd7e6 | ||
|
|
857c7b3a3a | ||
|
|
83cac93ca8 | ||
|
|
d34fa3e7df | ||
|
|
75cb8bb5ea | ||
|
|
2a99cc6a25 | ||
|
|
b5f98d1270 | ||
|
|
ef697319da | ||
|
|
efd68f9fad | ||
|
|
3a74e049ab | ||
|
|
3b1b308357 | ||
|
|
5a068431ed | ||
|
|
25f086a64f | ||
|
|
a93d4d7e9d | ||
|
|
54781cf51a | ||
|
|
ad7d19c317 | ||
|
|
58496e56d2 | ||
|
|
2c18a41613 | ||
|
|
c65eecc295 | ||
|
|
cac45d9ced | ||
|
|
02d42f87d3 | ||
|
|
9c439f8d0a | ||
|
|
cf998f03b6 | ||
|
|
f9d1f2414a | ||
|
|
13b25c0614 | ||
|
|
607b631114 | ||
|
|
99e88bb499 | ||
|
|
4f84cf011d | ||
|
|
417ca86c30 | ||
|
|
7df37f8254 | ||
|
|
0726a9f774 | ||
|
|
f8cc1d6046 | ||
|
|
9383d0bcfc | ||
|
|
f615990c00 | ||
|
|
8feb34e7a8 | ||
|
|
a5523b04ef | ||
|
|
af518fbd2b | ||
|
|
8c37d6923b | ||
|
|
299ef95f25 | ||
|
|
8f81b194d3 | ||
|
|
6bc3e31537 | ||
|
|
840b29860f | ||
|
|
15460c8f97 | ||
|
|
a77ac21e0d | ||
|
|
2b6d8f6824 | ||
|
+19 |
4c91beeda9 | ||
|
|
d0cf79a991 | ||
|
|
86986d6337 | ||
|
|
5408e81de7 | ||
|
|
178c3114d9 | ||
|
|
357a33f454 | ||
|
|
4fccda26c6 | ||
|
|
f94c01b452 | ||
|
|
3ca68e7788 | ||
|
|
a4ba312579 | ||
|
|
a227fd7028 | ||
|
|
6b44b5786e | ||
|
|
5feba46b53 | ||
|
|
0cbf53b7a7 | ||
|
|
321ccbf5e2 | ||
|
|
57268bf4ea | ||
|
|
56fd9c7813 | ||
|
|
32db70037d | ||
|
|
778823e623 | ||
|
|
545826d319 | ||
|
|
dbef1de286 | ||
|
|
b0bd3df226 | ||
|
|
167bcf2863 | ||
|
|
ff485d5ff8 | ||
|
|
184e4ddd83 | ||
|
|
aed2c7ca9b | ||
|
|
e1f9ccd337 | ||
|
|
df14561560 |
@@ -100,7 +100,7 @@ runs:
|
||||
id: version
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
|
||||
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
|
||||
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
|
||||
is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
|
||||
is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc...
|
||||
- Please do not check in files that don't have real changes
|
||||
- Please do not reformat lines that you didn't have to change the code on
|
||||
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version),
|
||||
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version),
|
||||
because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines.
|
||||
- If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description.
|
||||
- If your other co-developers have comments on your PR please tweak as needed.
|
||||
- Please also enable "Allow edits by maintainers".
|
||||
- Please do not submit untested code.
|
||||
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
|
||||
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes.
|
||||
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
|
||||
|
||||
## 🤝 Attestations
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||
id: gpg
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
PKG_VERSION: ${{ steps.version.outputs.deb }}
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
overwrite: true
|
||||
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-firmware
|
||||
with:
|
||||
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
release/device-*.bat
|
||||
|
||||
- name: Store manifests as an artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-manifest
|
||||
with:
|
||||
name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-*-*
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-*-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
|
||||
@@ -35,8 +35,8 @@ jobs:
|
||||
series:
|
||||
- jammy # 22.04 LTS
|
||||
- noble # 24.04 LTS
|
||||
- plucky # 25.04
|
||||
- questing # 25.10
|
||||
- resolute # 26.04 LTS
|
||||
uses: ./.github/workflows/package_ppa.yml
|
||||
with:
|
||||
ppa_repo: ppa:meshtastic/daily
|
||||
|
||||
@@ -60,16 +60,16 @@ jobs:
|
||||
|
||||
- name: Docker login
|
||||
if: ${{ inputs.push }}
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: meshtastic
|
||||
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Docker setup
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Sanitize platform string
|
||||
id: sanitize_platform
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Docker tag
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: meshtastic/meshtasticd
|
||||
tags: |
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
flavor: latest=false
|
||||
|
||||
- name: Docker build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: docker_variant
|
||||
with:
|
||||
context: .
|
||||
|
||||
@@ -139,14 +139,14 @@ jobs:
|
||||
id: tags
|
||||
|
||||
- name: Docker login
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: meshtastic
|
||||
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
|
||||
|
||||
- name: Docker meta (Debian)
|
||||
id: meta_debian
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: meshtastic/meshtasticd
|
||||
tags: |
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Docker meta (Alpine)
|
||||
id: meta_alpine
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: meshtastic/meshtasticd
|
||||
tags: |
|
||||
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
run: ls -R
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -250,13 +250,13 @@ jobs:
|
||||
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
|
||||
fetch-depth: 0
|
||||
- name: Download the current manifests
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./manifests-new/
|
||||
pattern: manifest-*
|
||||
merge-multiple: true
|
||||
- name: Upload combined manifests for later commit and global stats crunching.
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
id: upload-manifest
|
||||
with:
|
||||
name: manifests-${{ github.sha }}
|
||||
@@ -324,14 +324,14 @@ jobs:
|
||||
body: ${{ steps.release_notes.outputs.notes }}
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||
merge-multiple: true
|
||||
path: ./output/debian-src
|
||||
|
||||
- name: Download `native-tft` pio deps
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -355,7 +355,7 @@ jobs:
|
||||
}' > firmware-${{ needs.version.outputs.long }}.json
|
||||
|
||||
- name: Save Release manifest artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: manifest-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -413,7 +413,7 @@ jobs:
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -454,14 +454,14 @@ jobs:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Get firmware artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./publish
|
||||
|
||||
- name: Get manifest artifact
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: manifest-${{ needs.version.outputs.long }}
|
||||
path: ./publish
|
||||
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -235,14 +235,14 @@ jobs:
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||
merge-multiple: true
|
||||
path: ./output/debian-src
|
||||
|
||||
- name: Download `native-tft` pio deps
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
@@ -54,9 +54,19 @@ jobs:
|
||||
PLATFORMIO_LIBDEPS_DIR: pio/libdeps
|
||||
PLATFORMIO_PACKAGES_DIR: pio/packages
|
||||
PLATFORMIO_CORE_DIR: pio/core
|
||||
PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0
|
||||
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650
|
||||
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240
|
||||
|
||||
- name: Mangle platformio cache
|
||||
# Add "1" to epoch-timestamps of all downloads in the cache.
|
||||
# This is a hack to prevent internet access at build-time.
|
||||
run: |
|
||||
cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak
|
||||
jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
sudo apt-get install -y dput
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
|
||||
id: gpg
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Download test artifacts
|
||||
if: needs.native-tests.result != 'skipped'
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
series:
|
||||
- jammy # 22.04 LTS
|
||||
- noble # 24.04 LTS
|
||||
- plucky # 25.04
|
||||
- questing # 25.10
|
||||
- resolute # 26.04 LTS
|
||||
uses: ./.github/workflows/package_ppa.yml
|
||||
with:
|
||||
ppa_repo: |-
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# step 3
|
||||
- name: save report as pipeline artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: report.sarif
|
||||
overwrite: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v10.1.1
|
||||
uses: actions/stale@v10.2.0
|
||||
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.
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Save coverage information
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always() # run this step even if previous step failed
|
||||
with:
|
||||
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Save test results
|
||||
if: always() # run this step even if previous step failed
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
|
||||
|
||||
- name: Save coverage information
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always() # run this step even if previous step failed
|
||||
with:
|
||||
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
|
||||
@@ -137,20 +137,20 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: Test Report
|
||||
uses: dorny/test-reporter@v2.5.0
|
||||
uses: dorny/test-reporter@v2.6.0
|
||||
with:
|
||||
name: PlatformIO Tests
|
||||
path: testreport.xml
|
||||
reporter: java-junit
|
||||
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
|
||||
path: code-coverage-report
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
|
||||
|
||||
- name: Save Code Coverage Report
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage-report-${{ steps.version.outputs.long }}
|
||||
path: code-coverage-report
|
||||
|
||||
@@ -33,6 +33,7 @@ __pycache__
|
||||
*~
|
||||
|
||||
venv/
|
||||
.venv/
|
||||
release/
|
||||
.vscode/extensions.json
|
||||
/compile_commands.json
|
||||
|
||||
+11
-11
@@ -4,24 +4,24 @@ cli:
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.4
|
||||
ref: v1.7.5
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.501
|
||||
- renovate@43.10.3
|
||||
- checkov@3.2.507
|
||||
- renovate@43.57.0
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.93.3
|
||||
- trufflehog@3.93.7
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.3
|
||||
- trivy@0.69.1
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.1
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.47.0
|
||||
- ruff@0.15.5
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.0
|
||||
- svgo@4.0.0
|
||||
- actionlint@1.7.10
|
||||
- svgo@4.0.1
|
||||
- actionlint@1.7.11
|
||||
- flake8@7.3.0
|
||||
- hadolint@2.14.0
|
||||
- shfmt@3.6.0
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Lightweight container for running native PlatformIO tests on non-Linux hosts
|
||||
FROM python:3.14-slim-trixie
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
g++ git ca-certificates pkg-config \
|
||||
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
|
||||
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
|
||||
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir platformio==6.1.19 \
|
||||
&& useradd --create-home --shell /usr/sbin/nologin meshtastic
|
||||
|
||||
WORKDIR /firmware
|
||||
RUN chown -R meshtastic:meshtastic /firmware
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD platformio --version || exit 1
|
||||
|
||||
USER meshtastic
|
||||
|
||||
# Run tests by default; override with docker run args for specific filters
|
||||
CMD ["platformio", "test", "-e", "coverage", "-v"]
|
||||
+1
-1
@@ -23,4 +23,4 @@ for BOARD in $BOARDS; do
|
||||
CHECK="${CHECK} -e ${BOARD}"
|
||||
done
|
||||
|
||||
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high
|
||||
pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high
|
||||
|
||||
@@ -187,6 +187,7 @@ Logging:
|
||||
LogLevel: info # debug, info, warn, error
|
||||
# TraceFile: /var/log/meshtasticd.json
|
||||
# JSONFile: /packets.json # File location for JSON output of decoded packets
|
||||
# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation
|
||||
# JSONFilter: position # filter for packets to save to JSON file
|
||||
# AsciiLogs: true # default if not specified is !isatty() on stdout
|
||||
|
||||
@@ -214,3 +215,4 @@ General:
|
||||
AvailableDirectory: /etc/meshtasticd/available.d/
|
||||
# MACAddress: AA:BB:CC:DD:EE:FF
|
||||
# MACAddressSource: eth0
|
||||
# APIPort: 4403
|
||||
|
||||
@@ -6,6 +6,9 @@ Lora:
|
||||
Reset: 16 # IO4
|
||||
Busy: 24 # IO5
|
||||
# Ant_sw: 13 # IO3
|
||||
Enable_Pins:
|
||||
- 12
|
||||
- 13
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
|
||||
@@ -4,5 +4,8 @@ Lora:
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
@@ -0,0 +1,16 @@
|
||||
Lora:
|
||||
|
||||
### RAK13300in Slot 1
|
||||
Module: sx1262
|
||||
IRQ: 22 #IO6
|
||||
Reset: 16 # IO4
|
||||
Busy: 24 # IO5
|
||||
# Ant_sw: 13 # IO3
|
||||
Enable_Pins:
|
||||
- 12
|
||||
- 13
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# CS: 8
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -0,0 +1,12 @@
|
||||
Lora:
|
||||
### RAK13300in Slot 2 pins
|
||||
IRQ: 18 #IO6
|
||||
Reset: 24 # IO4
|
||||
Busy: 19 # IO5
|
||||
# Ant_sw: 23 # IO3
|
||||
Enable_Pins:
|
||||
- 26
|
||||
- 23
|
||||
spidev: spidev0.1
|
||||
# CS: 7
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -5,7 +5,11 @@ Lora:
|
||||
IRQ: 22 #IO6
|
||||
Reset: 16 # IO4
|
||||
Busy: 24 # IO5
|
||||
# Ant_sw: 13 # IO3
|
||||
Enable_Pins:
|
||||
- 12
|
||||
- 13
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
spidev: spidev0.0
|
||||
# GPIO_DETECT_PA: 13
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -0,0 +1,16 @@
|
||||
# For use with Armbian luckfox-lyra-ultra-w
|
||||
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
|
||||
# 1 Watt Lyra Ultra hat
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
CS: 10
|
||||
IRQ: 5
|
||||
Busy: 11
|
||||
Reset: 9
|
||||
RXen: 14
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
|
||||
spiSpeed: 2000000
|
||||
@@ -0,0 +1,17 @@
|
||||
# For use with Armbian luckfox-lyra-ultra-w
|
||||
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
|
||||
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
|
||||
# 2 Watt Lyra Ultra hat
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
SX126X_MAX_POWER: 8
|
||||
CS: 10
|
||||
IRQ: 5
|
||||
Busy: 11
|
||||
Reset: 9
|
||||
RXen: 14
|
||||
|
||||
spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7)
|
||||
spiSpeed: 2000000
|
||||
@@ -0,0 +1,25 @@
|
||||
# For use with Armbian luckfox-lyra // luckfox-lyra-plus
|
||||
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
|
||||
# Waveshare LoRa HAT for Raspberry Pi Pico
|
||||
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
|
||||
Lora:
|
||||
Module: sx1262
|
||||
DIO2_AS_RF_SWITCH: true
|
||||
DIO3_TCXO_VOLTAGE: true
|
||||
spidev: spidev0.0
|
||||
CS: # GPIO0_B5
|
||||
pin: 13
|
||||
gpiochip: 0
|
||||
line: 13
|
||||
IRQ: # GPIO1_C2
|
||||
pin: 50
|
||||
gpiochip: 1
|
||||
line: 18
|
||||
Busy: # GPIO0_B4
|
||||
pin: 12
|
||||
gpiochip: 0
|
||||
line: 12
|
||||
Reset: # GPIO0_A2
|
||||
pin: 2
|
||||
gpiochip: 0
|
||||
line: 2
|
||||
@@ -13,8 +13,7 @@ Lora:
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
|
||||
@@ -13,8 +13,7 @@ Lora:
|
||||
# USB_Serialnum: 12345678
|
||||
SX126X_MAX_POWER: 22
|
||||
# Reduce output power to improve EMI
|
||||
NUM_PA_POINTS: 22
|
||||
TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7
|
||||
TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
|
||||
# Note: This module integrates an additional PA to achieve higher output power.
|
||||
# The 'power' parameter here does not represent the actual RF output.
|
||||
# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm).
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts).
|
||||
#
|
||||
# Usage:
|
||||
# ./bin/test-native-docker.sh # run all native tests
|
||||
# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter
|
||||
# ./bin/test-native-docker.sh --rebuild # force rebuild the image
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
IMAGE_NAME="meshtastic-native-test"
|
||||
|
||||
REBUILD=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--rebuild" ]]; then
|
||||
REBUILD=true
|
||||
else
|
||||
EXTRA_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
|
||||
echo "Building test image (first run may take a few minutes)..."
|
||||
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR"
|
||||
fi
|
||||
|
||||
# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI)
|
||||
sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/'
|
||||
|
||||
# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through.
|
||||
if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then
|
||||
CMD=("platformio" "test" "-e" "coverage" "-v")
|
||||
else
|
||||
CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}")
|
||||
fi
|
||||
|
||||
exec docker run --rm \
|
||||
-v "$ROOT_DIR:/src:ro" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}"
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default.csv"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_ESP32S3_DEV",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "4MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 4194304,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.lilygo.cc",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
|
||||
+1
-1
@@ -12,6 +12,6 @@ Ex:
|
||||
- `logo_320x480.png`
|
||||
- `logo_320x240.png`
|
||||
|
||||
This file is copied to `data/boot/logo.png` before filesytem image compilation.
|
||||
This file is copied to `data/boot/logo.png` before filesystem image compilation.
|
||||
|
||||
For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33).
|
||||
|
||||
Vendored
+3
@@ -3,6 +3,9 @@ export DEBEMAIL="jbennett@incomsystems.biz"
|
||||
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
||||
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||
export PLATFORMIO_CORE_DIR=pio/core
|
||||
export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0
|
||||
export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650
|
||||
export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
|
||||
|
||||
# Download libraries to `pio`
|
||||
platformio pkg install -e native-tft
|
||||
|
||||
Vendored
+4
-1
@@ -9,7 +9,10 @@
|
||||
PIO_ENV:=\
|
||||
PLATFORMIO_CORE_DIR=pio/core \
|
||||
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
|
||||
PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||
PLATFORMIO_PACKAGES_DIR=pio/packages \
|
||||
PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \
|
||||
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \
|
||||
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
|
||||
|
||||
# Raspbian armhf builds should be compatible with armv6-hardfloat
|
||||
# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
# BLE Mesh Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Add BLE as a mesh transport alongside LoRa, enabling Meshtastic nodes to relay packets over Bluetooth Low Energy. Follow the same handler pattern used by UDP multicast ([UdpMulticastHandler](src/mesh/udp/UdpMulticastHandler.h)) — a standalone handler class with `onSend()` hooked into `Router::send()` and received packets injected via `router->enqueueReceivedMessage()`. No changes to `RadioInterface` or Router architecture needed.
|
||||
|
||||
BLE mesh is useful for:
|
||||
|
||||
- Dense indoor deployments where LoRa range is excessive
|
||||
- Extending mesh reach through devices that lack LoRa hardware
|
||||
- Hybrid LoRa+BLE networks (gateway nodes bridge both transports)
|
||||
- Lower latency links between nearby nodes
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Pattern: Same as UDP Multicast
|
||||
|
||||
```
|
||||
Router::send()
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
iface->send(p) udpHandler->onSend(p) bleMeshHandler->onSend(p)
|
||||
(LoRa) (UDP) (BLE mesh)
|
||||
|
||||
Router::enqueueReceivedMessage()
|
||||
▲
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
LoRa RX ISR udpHandler->onReceive() bleMeshHandler->onReceive()
|
||||
```
|
||||
|
||||
The existing UDP pattern ([Router.cpp:378-382](src/mesh/Router.cpp#L378-L382)):
|
||||
|
||||
```cpp
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler && config.network.enabled_protocols & ...) {
|
||||
udpHandler->onSend(const_cast<meshtastic_MeshPacket *>(p));
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
BLE mesh hooks in identically, right next to it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Protobuf & Configuration
|
||||
|
||||
### 1.1 Transport mechanism enum
|
||||
|
||||
**File:** protobufs repo (`mesh.proto`)
|
||||
|
||||
Add a new transport type alongside the existing ones:
|
||||
|
||||
```protobuf
|
||||
enum TransportMechanism {
|
||||
// ... existing 0-7 ...
|
||||
TRANSPORT_BLE_MESH = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 BLE Mesh configuration
|
||||
|
||||
Add BLE mesh config to the device config protobuf:
|
||||
|
||||
```protobuf
|
||||
message BleMeshConfig {
|
||||
bool enabled = 1;
|
||||
|
||||
// Operating mode
|
||||
enum Mode {
|
||||
OFF = 0;
|
||||
ADVERTISE_AND_SCAN = 1; // connectionless (extended adv)
|
||||
GATT = 2; // connection-based
|
||||
HYBRID = 3; // both
|
||||
}
|
||||
Mode mode = 2;
|
||||
|
||||
// Scan/advertising interval in ms (latency vs power tradeoff)
|
||||
uint32_t interval_ms = 3; // default: 500ms
|
||||
|
||||
// TX power level (platform-mapped)
|
||||
int8_t tx_power = 4; // dBm
|
||||
|
||||
// Max BLE mesh peers to track
|
||||
uint32_t max_peers = 5; // default: 8
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: BLE Mesh Handler — Core Class
|
||||
|
||||
### 2.1 Base handler (platform-agnostic)
|
||||
|
||||
**New file:** `src/mesh/BLEMeshHandler.h`
|
||||
|
||||
Follow the `UdpMulticastHandler` pattern — a simple class, not a `RadioInterface`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH
|
||||
|
||||
#include "configuration.h"
|
||||
#include "mesh/Router.h"
|
||||
|
||||
class BLEMeshHandler {
|
||||
public:
|
||||
BLEMeshHandler() : isRunning(false) {}
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
// Called from Router::send() — broadcast packet over BLE
|
||||
virtual bool onSend(const meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
protected:
|
||||
bool isRunning;
|
||||
|
||||
// Shared RX path: decode and enqueue into router
|
||||
void deliverToRouter(const uint8_t *data, size_t len) {
|
||||
meshtastic_MeshPacket mp;
|
||||
if (!pb_decode_from_bytes(data, len, &meshtastic_MeshPacket_msg, &mp))
|
||||
return;
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
|
||||
return;
|
||||
|
||||
mp.transport_mechanism =
|
||||
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH;
|
||||
mp.rx_snr = 0;
|
||||
mp.rx_rssi = 0;
|
||||
|
||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||
if (router)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
// Shared TX path: encode packet to bytes
|
||||
size_t encodeForBLE(const meshtastic_MeshPacket *mp, uint8_t *buf, size_t bufLen) {
|
||||
return pb_encode_to_bytes(buf, bufLen, &meshtastic_MeshPacket_msg, mp);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH
|
||||
```
|
||||
|
||||
### 2.2 Wire format
|
||||
|
||||
Same as UDP multicast — protobuf-encoded `meshtastic_MeshPacket` (already encrypted). This means:
|
||||
|
||||
- Packets are encoded with `pb_encode_to_bytes()` on TX
|
||||
- Decoded with `pb_decode_from_bytes()` on RX
|
||||
- Encryption already applied by Router before `onSend()` is called
|
||||
- Same deduplication via `PacketHistory` (keyed on `sender, id`)
|
||||
|
||||
### 2.3 Echo prevention
|
||||
|
||||
Same pattern as UDP ([UdpMulticastHandler.h:102](src/mesh/udp/UdpMulticastHandler.h#L102)):
|
||||
|
||||
```cpp
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_ERROR("Attempt to send BLE-sourced packet over BLE");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: ESP32 Implementation (NimBLE)
|
||||
|
||||
### Platform: ESP32, ESP32-S3, ESP32-C3, ESP32-C6
|
||||
|
||||
**New files:** `src/nimble/NimbleBLEMesh.h` / `NimbleBLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
|
||||
class NimbleBLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// --- Advertising mode ---
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static void onScanResult(const ble_gap_disc_desc *desc, void *arg);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// --- GATT mode ---
|
||||
void setupMeshService();
|
||||
static int onMeshCharWrite(uint16_t conn_handle,
|
||||
const ble_gatt_error *error,
|
||||
ble_gatt_attr *attr, void *arg);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// --- Peer tracking ---
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 if not connected
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
void updatePeer(const ble_addr_t &addr, NodeNum nodeNum, int8_t rssi);
|
||||
void pruneStale();
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 3.1 Advertising mode (connectionless)
|
||||
|
||||
Encode mesh packets into BLE extended advertisements with Meshtastic manufacturer data.
|
||||
|
||||
**TX flow:**
|
||||
|
||||
1. `onSend()` called from `Router::send()`
|
||||
2. Encode packet via `encodeForBLE()`
|
||||
3. Pack into extended advertisement manufacturer data
|
||||
4. Broadcast for configurable number of intervals
|
||||
|
||||
**RX flow:**
|
||||
|
||||
1. NimBLE scan callback fires
|
||||
2. Check manufacturer ID for Meshtastic magic
|
||||
3. Extract protobuf bytes
|
||||
4. Call `deliverToRouter()`
|
||||
|
||||
**Advertisement structure:**
|
||||
|
||||
```
|
||||
AD Type: 0xFF (Manufacturer Specific)
|
||||
Company ID: 0x????? (Meshtastic BLE SIG ID or 0xFFFF)
|
||||
Mesh Version: 1 byte
|
||||
Protobuf payload: variable (up to ~244 bytes with extended adv)
|
||||
```
|
||||
|
||||
**Key NimBLE APIs:**
|
||||
|
||||
- `ble_gap_ext_adv_set_data()` / `ble_gap_ext_adv_start()` — extended adv TX
|
||||
- `ble_gap_disc()` or `ble_gap_ext_disc()` — scanning
|
||||
- Scan callback receives `ble_gap_disc_desc` or `ble_gap_ext_disc_desc`
|
||||
|
||||
### 3.2 GATT mode (connection-based)
|
||||
|
||||
Add a new GATT service for mesh relay, separate from the phone API service:
|
||||
|
||||
**New Mesh Relay Service:**
|
||||
|
||||
```
|
||||
Mesh Relay Service UUID: (new, distinct from phone service in BluetoothCommon.h)
|
||||
├── MeshTX (WRITE_NO_RSP) — peers write mesh packets to us
|
||||
├── MeshRX (NOTIFY) — we notify peers of new mesh packets
|
||||
└── MeshPeer (READ) — our NodeNum for discovery
|
||||
```
|
||||
|
||||
**TX:** Write to MeshTX on all connected peers (broadcast) or specific next-hop peer (directed).
|
||||
**RX:** MeshTX write callback → `deliverToRouter()`.
|
||||
|
||||
### 3.3 Coexistence with phone BLE
|
||||
|
||||
Both services registered on the same NimBLE host:
|
||||
|
||||
- Phone API service: existing UUIDs ([BluetoothCommon.h](src/BluetoothCommon.h))
|
||||
- Mesh relay service: new UUIDs
|
||||
- Separate connection handles for phone vs mesh peers
|
||||
- Use extended adv set 0 for phone, set 1 for mesh (NimBLE supports multiple adv sets)
|
||||
- Reserve 1 connection for phone, remaining for mesh peers
|
||||
|
||||
### 3.4 ESP32 variant considerations
|
||||
|
||||
| Variant | BLE | Extended Adv | Max Conn | Strategy |
|
||||
| -------- | --- | ------------ | -------- | --------------------------------------- |
|
||||
| ESP32 | 4.2 | No | 9 | GATT-only (legacy adv too small at 31B) |
|
||||
| ESP32-S3 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
| ESP32-C3 | 5.0 | Yes | 3 | Adv preferred (limited connections) |
|
||||
| ESP32-C6 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: nRF52 Implementation (SoftDevice/Bluefruit)
|
||||
|
||||
### Platform: nRF52840 (primary), nRF52833
|
||||
|
||||
**New files:** `src/platform/nrf52/NRF52BLEMesh.h` / `NRF52BLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
class NRF52BLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// Advertising mode
|
||||
void startScanning();
|
||||
static void onScanCallback(ble_gap_evt_adv_report_t *report);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// GATT mode
|
||||
BLEService meshService;
|
||||
BLECharacteristic meshTxChar;
|
||||
BLECharacteristic meshRxChar;
|
||||
void setupMeshService();
|
||||
static void onMeshWrite(uint16_t connHandle, BLECharacteristic *chr,
|
||||
uint8_t *data, uint16_t len);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// Peers
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_gap_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle;
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 4.1 Advertising mode
|
||||
|
||||
nRF52840 supports BLE 5.0 extended advertisements (up to 247 bytes).
|
||||
|
||||
**Bluefruit APIs:**
|
||||
|
||||
- `Bluefruit.Scanner.setRxCallback()` — scan result callback
|
||||
- `Bluefruit.Scanner.start()` / `.stop()` — control scanning
|
||||
- `Bluefruit.Advertising` — for standard adv
|
||||
- Raw SoftDevice `sd_ble_gap_adv_set_configure()` if Bluefruit doesn't expose extended adv data
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Use Bluefruit Scanner for RX (filter by Meshtastic manufacturer ID)
|
||||
- For TX, either use Bluefruit Advertising API or drop to raw SoftDevice for extended adv payloads
|
||||
|
||||
### 4.2 GATT mode
|
||||
|
||||
Same service structure as ESP32, using Bluefruit GATT APIs:
|
||||
|
||||
```cpp
|
||||
meshService = BLEService(MESH_RELAY_SERVICE_UUID);
|
||||
meshTxChar = BLECharacteristic(MESH_TX_CHAR_UUID);
|
||||
meshTxChar.setProperties(CHR_PROPS_WRITE_WO_RESP);
|
||||
meshTxChar.setWriteCallback(onMeshWrite);
|
||||
meshTxChar.setMaxLen(512);
|
||||
|
||||
meshRxChar = BLECharacteristic(MESH_RX_CHAR_UUID);
|
||||
meshRxChar.setProperties(CHR_PROPS_NOTIFY);
|
||||
meshRxChar.setMaxLen(512);
|
||||
```
|
||||
|
||||
**Peer connections:**
|
||||
|
||||
- nRF52840 SoftDevice S140 supports up to 20 concurrent connections
|
||||
- Use `Bluefruit.Central` for outbound connections to mesh peers
|
||||
- Bluefruit supports simultaneous peripheral (phone) + central (mesh peers)
|
||||
|
||||
### 4.3 Coexistence with phone BLE
|
||||
|
||||
Existing phone service ([NRF52Bluetooth.cpp](src/platform/nrf52/NRF52Bluetooth.cpp)) uses peripheral role:
|
||||
|
||||
- Keep phone API service as-is
|
||||
- Add mesh relay service alongside it
|
||||
- Scanner shared between peer discovery and mesh RX (filter by service UUID or manufacturer data)
|
||||
- SoftDevice connection budget: 1 peripheral (phone) + N central (mesh peers)
|
||||
|
||||
### 4.4 nRF52 extras
|
||||
|
||||
| Feature | nRF52840 | nRF52833 |
|
||||
| ---------------------- | -------- | -------- |
|
||||
| Extended Adv | Yes | Yes |
|
||||
| Max Connections | 20 | 20 |
|
||||
| RAM per connection | ~1.8KB | ~1.8KB |
|
||||
| Coded PHY (Long Range) | Yes | Yes |
|
||||
|
||||
**Coded PHY:** BLE Coded PHY (125kbps) provides ~4x range vs 1Mbps PHY. Could be used for extended-range BLE mesh links as a configuration option.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Router Integration
|
||||
|
||||
### 5.1 Hook into Router::send()
|
||||
|
||||
**File:** [src/mesh/Router.cpp](src/mesh/Router.cpp) — add right next to UDP handler:
|
||||
|
||||
```cpp
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler && config.bluetooth.mesh_enabled) {
|
||||
bleMeshHandler->onSend(p);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.2 Global handler instance
|
||||
|
||||
**File:** [src/main.h](src/main.h):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
extern BLEMeshHandler *bleMeshHandler;
|
||||
#endif
|
||||
```
|
||||
|
||||
**File:** [src/main.cpp](src/main.cpp):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
BLEMeshHandler *bleMeshHandler = nullptr;
|
||||
#endif
|
||||
|
||||
// In setup():
|
||||
#ifdef HAS_BLE_MESH
|
||||
#if defined(ARCH_ESP32)
|
||||
bleMeshHandler = new NimbleBLEMesh();
|
||||
#elif defined(ARCH_NRF52)
|
||||
bleMeshHandler = new NRF52BLEMesh();
|
||||
#endif
|
||||
bleMeshHandler->start();
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.3 NODENUM_BROADCAST_NO_LORA
|
||||
|
||||
Already reserved ([MeshTypes.h:13-14](src/mesh/MeshTypes.h#L13-L14)) and already dropped by LoRa ([RadioLibInterface.cpp:170](src/mesh/RadioLibInterface.cpp#L170)). BLE mesh handler should still send packets addressed to `NODENUM_BROADCAST_NO_LORA` — this is its purpose.
|
||||
|
||||
### 5.4 Feature flag
|
||||
|
||||
**File:** `src/configuration.h` or per-variant `platformio.ini`:
|
||||
|
||||
```cpp
|
||||
#define HAS_BLE_MESH 1 // Enable BLE mesh transport
|
||||
```
|
||||
|
||||
Disabled by default. Enabled per-board in variant configs for boards with sufficient BLE capability.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Peer Discovery & Management
|
||||
|
||||
### 6.1 Discovery methods
|
||||
|
||||
1. **Passive scanning**: Listen for Meshtastic manufacturer data in BLE advertisements
|
||||
2. **Service discovery**: Scan for Mesh Relay Service UUID (GATT mode)
|
||||
3. **NodeDB integration**: Update `meshtastic_NodeInfoLite` with BLE reachability when a peer is discovered
|
||||
|
||||
### 6.2 Peer table
|
||||
|
||||
```cpp
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
// Platform-specific BLE address (ble_addr_t on ESP32, ble_gap_addr_t on nRF52)
|
||||
uint8_t addr[6];
|
||||
uint8_t addrType;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 = not connected
|
||||
uint8_t failedConnAttempts;
|
||||
};
|
||||
```
|
||||
|
||||
Max peers: configurable (default 8), bounded by platform connection limits.
|
||||
|
||||
### 6.3 Connection policy (GATT mode)
|
||||
|
||||
- Auto-connect if RSSI > threshold and slots available
|
||||
- Disconnect stale peers (no traffic for N minutes)
|
||||
- Prefer advertising for broadcasts, GATT for directed/reliable
|
||||
- Exponential backoff on failed connection attempts
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Power Management
|
||||
|
||||
### 7.1 Scan duty cycling
|
||||
|
||||
```
|
||||
Active scanning: ~15mA (nRF52), ~90mA (ESP32)
|
||||
Advertising: ~5mA peak per event
|
||||
Idle: ~0mA additional
|
||||
|
||||
Default strategy:
|
||||
- Scan window: 50ms every 500ms (10% duty cycle)
|
||||
- ~1.5mA average on nRF52, ~9mA on ESP32
|
||||
- Configurable via BleMeshConfig.interval_ms
|
||||
```
|
||||
|
||||
### 7.2 Sleep integration
|
||||
|
||||
- Light sleep: pause scanning, continue advertising at reduced rate
|
||||
- Deep sleep: disable BLE mesh entirely
|
||||
- Hook into existing power management observers
|
||||
|
||||
### 7.3 Role-based behavior
|
||||
|
||||
| Device Role | BLE Mesh Behavior |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| CLIENT | Scan on-demand, advertise when sending |
|
||||
| CLIENT_MUTE | Receive only, no rebroadcast |
|
||||
| ROUTER | Continuous scan+advertise, max connections |
|
||||
| ROUTER_LATE | Delayed rebroadcast (same as LoRa behavior) |
|
||||
| SENSOR | Minimal: advertise telemetry only |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Testing & Validation
|
||||
|
||||
### 8.1 Unit tests
|
||||
|
||||
- BLE wire format encode/decode roundtrip
|
||||
- PacketHistory deduplication across LoRa + BLE
|
||||
- Peer table management (add, prune, limits)
|
||||
- Echo prevention (don't re-send BLE-sourced packets over BLE)
|
||||
|
||||
### 8.2 Integration tests
|
||||
|
||||
- Two ESP32 nodes: BLE-only mesh (no LoRa)
|
||||
- Two nRF52 nodes: BLE-only mesh
|
||||
- Three nodes: multi-hop BLE relay
|
||||
- Hybrid: Node A (LoRa+BLE) <-> Node B (BLE only) <-> Node C (LoRa+BLE)
|
||||
- Phone API + BLE mesh coexistence
|
||||
- Power consumption profiling
|
||||
|
||||
### 8.3 Compatibility matrix
|
||||
|
||||
| Test | ESP32 | ESP32-S3 | ESP32-C3 | ESP32-C6 | nRF52840 |
|
||||
| ------------- | ----------- | -------- | -------- | -------- | -------- |
|
||||
| Adv mode | GATT only\* | Yes | Yes | Yes | Yes |
|
||||
| GATT mode | Yes | Yes | Yes | Yes | Yes |
|
||||
| Phone coexist | Yes | Yes | Yes | Yes | Yes |
|
||||
| Multi-hop | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
\*ESP32 (BLE 4.2): legacy adv limited to 31B, use GATT-only mode
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Milestone 1: Foundation
|
||||
|
||||
1. Protobuf additions (`TRANSPORT_BLE_MESH`, `BleMeshConfig`)
|
||||
2. `BLEMeshHandler` base class
|
||||
3. Router hook (alongside UDP handler)
|
||||
4. Feature flag + main.cpp wiring
|
||||
|
||||
### Milestone 2: ESP32 MVP
|
||||
|
||||
5. `NimbleBLEMesh` — advertising mode on ESP32-S3
|
||||
6. Phone BLE coexistence validation
|
||||
7. Basic two-node BLE mesh test
|
||||
|
||||
### Milestone 3: nRF52 MVP
|
||||
|
||||
8. `NRF52BLEMesh` — advertising mode
|
||||
9. Cross-platform interop (ESP32 <-> nRF52 BLE mesh)
|
||||
|
||||
### Milestone 4: GATT Mode
|
||||
|
||||
10. GATT service + connection management for ESP32
|
||||
11. GATT service + connection management for nRF52
|
||||
12. Peer discovery & table management
|
||||
|
||||
### Milestone 5: Polish
|
||||
|
||||
13. Power management integration
|
||||
14. Full test matrix
|
||||
15. ESP32 (BLE 4.2) GATT-only fallback
|
||||
16. Admin UI / config in phone apps
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| -------------------------------- | ------------------------- | ---------------------------------------------------------------------------- |
|
||||
| BLE scan power drain | Battery life | Duty cycling, role-based policy |
|
||||
| Phone BLE drops during mesh scan | UX regression | Separate adv sets, careful interleaving |
|
||||
| NimBLE/Bluefruit API differences | Platform code duplication | Thin platform subclasses, shared base logic |
|
||||
| No extended adv on ESP32 (4.2) | Can't fit packets in adv | GATT-only fallback |
|
||||
| BLE range (~30-100m) | Coverage gaps | Hybrid LoRa+BLE; Coded PHY on nRF52 |
|
||||
| Packet storms in dense BLE mesh | Saturation | Existing flood mitigation (PacketHistory, hop_limit, role-based rebroadcast) |
|
||||
|
||||
---
|
||||
|
||||
## Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------- |
|
||||
| `src/mesh/BLEMeshHandler.h` | Base handler class (like UdpMulticastHandler) |
|
||||
| `src/nimble/NimbleBLEMesh.h` | ESP32 BLE mesh header |
|
||||
| `src/nimble/NimbleBLEMesh.cpp` | ESP32 NimBLE implementation |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.h` | nRF52 BLE mesh header |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.cpp` | nRF52 Bluefruit implementation |
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
| `src/mesh/Router.cpp` | Add `bleMeshHandler->onSend()` next to UDP handler |
|
||||
| `src/main.h` | Declare `extern BLEMeshHandler *bleMeshHandler` |
|
||||
| `src/main.cpp` | Instantiate + start BLE mesh handler |
|
||||
| `src/configuration.h` | `HAS_BLE_MESH` feature flag |
|
||||
| Protobufs (`mesh.proto`) | `TRANSPORT_BLE_MESH` enum + `BleMeshConfig` |
|
||||
| `src/nimble/NimbleBluetooth.cpp` | Coexistence (adv set separation) |
|
||||
| `src/platform/nrf52/NRF52Bluetooth.cpp` | Coexistence (scanner sharing) |
|
||||
| Platform `variant.h` / `platformio.ini` | Enable per-board |
|
||||
+22
-38
@@ -117,7 +117,7 @@ lib_deps =
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||
jgromes/RadioLib@7.5.0
|
||||
jgromes/RadioLib@7.6.0
|
||||
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
@@ -138,7 +138,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library
|
||||
adafruit/Adafruit BME280 Library@2.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310
|
||||
adafruit/Adafruit DPS310@1.1.5
|
||||
adafruit/Adafruit DPS310@1.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
|
||||
adafruit/Adafruit MCP9808 Library@2.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
|
||||
@@ -150,7 +150,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH
|
||||
adafruit/Adafruit LIS3DH@1.3.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0
|
||||
adafruit/Adafruit AHTX0@2.0.5
|
||||
adafruit/Adafruit AHTX0@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS
|
||||
adafruit/Adafruit LSM6DS@4.7.4
|
||||
# renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library
|
||||
@@ -158,7 +158,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632
|
||||
emotibit/EmotiBit MLX90632@1.0.8
|
||||
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
|
||||
adafruit/Adafruit MLX90614 Library@2.1.5
|
||||
adafruit/Adafruit MLX90614 Library@2.1.6
|
||||
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
||||
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
||||
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
||||
@@ -180,12 +180,12 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
||||
dfrobot/DFRobot_BMM150@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
|
||||
adafruit/Adafruit TSL2561@1.1.2
|
||||
adafruit/Adafruit TSL2561@1.1.3
|
||||
# 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]
|
||||
; Common environmental sensor libraries (not included in native / portduino)
|
||||
[environmental_extra_common]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
@@ -205,10 +205,6 @@ lib_deps =
|
||||
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
|
||||
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
|
||||
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
|
||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
@@ -217,33 +213,21 @@ lib_deps =
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30
|
||||
sensirion/Sensirion I2C SCD30@1.0.0
|
||||
|
||||
; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
; Environmental sensors with BSEC2 (Bosch proprietary IAQ)
|
||||
[environmental_extra]
|
||||
lib_deps =
|
||||
${environmental_extra_common.lib_deps}
|
||||
# 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
|
||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||
|
||||
; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets)
|
||||
[environmental_extra_no_bsec]
|
||||
lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||
adafruit/Adafruit BMP3XX Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||
adafruit/Adafruit MAX1704X@1.0.3
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||
adafruit/Adafruit LPS2X@2.0.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||
adafruit/Adafruit SHT31 Library@2.2.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library
|
||||
adafruit/Adafruit VEML7700 Library@2.1.6
|
||||
# renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library
|
||||
adafruit/Adafruit SHT4x Library@1.0.5
|
||||
# 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/ClosedCube OPT3001@1.1.2
|
||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||
sensirion/Sensirion Core@0.7.3
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||
# renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x
|
||||
sensirion/Sensirion I2C SFA3x@1.0.0
|
||||
${environmental_extra_common.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
|
||||
adafruit/Adafruit BME680 Library@^2.0.5
|
||||
|
||||
+1
-1
Submodule protobufs updated: 44298d374f...2edc5ab7b1
@@ -7,6 +7,8 @@
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef HAS_NCP5623
|
||||
#include <Wire.h>
|
||||
|
||||
#include <NCP5623.h>
|
||||
#endif
|
||||
|
||||
|
||||
+11
-16
@@ -4,6 +4,7 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include <memory>
|
||||
|
||||
#ifdef HAS_I2S
|
||||
#include <AudioFileSourcePROGMEM.h>
|
||||
@@ -29,9 +30,9 @@ class AudioThread : public concurrency::OSThread
|
||||
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||
#endif
|
||||
setCPUFast(true);
|
||||
rtttlFile = new AudioFileSourcePROGMEM(data, len);
|
||||
i2sRtttl = new AudioGeneratorRTTTL();
|
||||
i2sRtttl->begin(rtttlFile, audioOut);
|
||||
rtttlFile = std::unique_ptr<AudioFileSourcePROGMEM>(new AudioFileSourcePROGMEM(data, len));
|
||||
i2sRtttl = std::unique_ptr<AudioGeneratorRTTTL>(new AudioGeneratorRTTTL());
|
||||
i2sRtttl->begin(rtttlFile.get(), audioOut.get());
|
||||
}
|
||||
|
||||
// Also handles actually playing the RTTTL, needs to be called in loop
|
||||
@@ -47,14 +48,10 @@ class AudioThread : public concurrency::OSThread
|
||||
{
|
||||
if (i2sRtttl != nullptr) {
|
||||
i2sRtttl->stop();
|
||||
delete i2sRtttl;
|
||||
i2sRtttl = nullptr;
|
||||
}
|
||||
|
||||
if (rtttlFile != nullptr) {
|
||||
delete rtttlFile;
|
||||
rtttlFile = nullptr;
|
||||
}
|
||||
rtttlFile = nullptr;
|
||||
|
||||
setCPUFast(false);
|
||||
#ifdef T_LORA_PAGER
|
||||
@@ -66,16 +63,14 @@ class AudioThread : public concurrency::OSThread
|
||||
{
|
||||
if (i2sRtttl != nullptr) {
|
||||
i2sRtttl->stop();
|
||||
delete i2sRtttl;
|
||||
i2sRtttl = nullptr;
|
||||
}
|
||||
|
||||
#ifdef T_LORA_PAGER
|
||||
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||
#endif
|
||||
ESP8266SAM *sam = new ESP8266SAM;
|
||||
sam->Say(audioOut, text);
|
||||
delete sam;
|
||||
auto sam = std::unique_ptr<ESP8266SAM>(new ESP8266SAM);
|
||||
sam->Say(audioOut.get(), text);
|
||||
setCPUFast(false);
|
||||
#ifdef T_LORA_PAGER
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
@@ -96,15 +91,15 @@ class AudioThread : public concurrency::OSThread
|
||||
private:
|
||||
void initOutput()
|
||||
{
|
||||
audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S);
|
||||
audioOut = std::unique_ptr<AudioOutputI2S>(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S));
|
||||
audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
|
||||
audioOut->SetGain(0.2);
|
||||
};
|
||||
|
||||
AudioGeneratorRTTTL *i2sRtttl = nullptr;
|
||||
AudioOutputI2S *audioOut = nullptr;
|
||||
std::unique_ptr<AudioGeneratorRTTTL> i2sRtttl = nullptr;
|
||||
std::unique_ptr<AudioOutputI2S> audioOut = nullptr;
|
||||
|
||||
AudioFileSourcePROGMEM *rtttlFile = nullptr;
|
||||
std::unique_ptr<AudioFileSourcePROGMEM> rtttlFile = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
+5
-3
@@ -704,11 +704,11 @@ bool Power::setup()
|
||||
found = true;
|
||||
} else if (analogInit()) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
#ifdef NRF_APM
|
||||
found = true;
|
||||
found = true;
|
||||
#endif
|
||||
}
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
@@ -846,8 +846,10 @@ void Power::readPowerStatus()
|
||||
|
||||
if (batteryLevel) {
|
||||
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
|
||||
#ifndef NRF_APM
|
||||
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
|
||||
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
|
||||
#endif
|
||||
if (hasBattery) {
|
||||
batteryVoltageMv = batteryLevel->getBattVoltage();
|
||||
// If the AXP192 returns a valid battery percentage, use it
|
||||
|
||||
+8
-2
@@ -38,7 +38,10 @@ static bool isPowered()
|
||||
return true;
|
||||
#endif
|
||||
|
||||
bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0);
|
||||
bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
|
||||
? 1
|
||||
: 0);
|
||||
|
||||
// If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON
|
||||
// We assume routers might be powered all the time, but from a low current (solar) source
|
||||
@@ -262,7 +265,10 @@ Fsm powerFSM(&stateBOOT);
|
||||
|
||||
void PowerFSM_setup()
|
||||
{
|
||||
bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0);
|
||||
bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
||||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
|
||||
? 1
|
||||
: 0);
|
||||
bool hasPower = isPowered();
|
||||
|
||||
LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0);
|
||||
|
||||
+18
-29
@@ -227,34 +227,21 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
||||
#endif
|
||||
if (isBleConnected) {
|
||||
char *message;
|
||||
size_t initialLen;
|
||||
size_t len;
|
||||
initialLen = strlen(format);
|
||||
message = new char[initialLen + 1];
|
||||
len = vsnprintf(message, initialLen + 1, format, arg);
|
||||
if (len > initialLen) {
|
||||
delete[] message;
|
||||
message = new char[len + 1];
|
||||
vsnprintf(message, len + 1, format, arg);
|
||||
}
|
||||
auto thread = concurrency::OSThread::currentThread;
|
||||
meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero;
|
||||
logRecord.level = getLogLevel(logLevel);
|
||||
strcpy(logRecord.message, message);
|
||||
vsprintf(logRecord.message, format, arg);
|
||||
if (thread)
|
||||
strcpy(logRecord.source, thread->ThreadName.c_str());
|
||||
logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
|
||||
uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size];
|
||||
size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
#ifdef ARCH_ESP32
|
||||
nimbleBluetooth->sendLog(buffer, size);
|
||||
nimbleBluetooth->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF52)
|
||||
nrf52Bluetooth->sendLog(buffer, size);
|
||||
nrf52Bluetooth->sendLog(buffer.get(), size);
|
||||
#endif
|
||||
delete[] message;
|
||||
delete[] buffer;
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -292,8 +279,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
|
||||
// append \n to format
|
||||
size_t len = strlen(format);
|
||||
char *newFormat = new char[len + 2];
|
||||
strcpy(newFormat, format);
|
||||
auto newFormat = std::unique_ptr<char[]>(new char[len + 2]);
|
||||
strcpy(newFormat.get(), format);
|
||||
newFormat[len] = '\n';
|
||||
newFormat[len + 1] = '\0';
|
||||
|
||||
@@ -310,23 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
va_end(arg);
|
||||
}
|
||||
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,11 +320,19 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
#endif
|
||||
|
||||
va_list arg;
|
||||
va_list arg_copy;
|
||||
|
||||
va_start(arg, format);
|
||||
|
||||
log_to_serial(logLevel, newFormat, arg);
|
||||
log_to_syslog(logLevel, newFormat, arg);
|
||||
log_to_ble(logLevel, newFormat, arg);
|
||||
va_copy(arg_copy, arg);
|
||||
log_to_serial(logLevel, newFormat.get(), arg_copy);
|
||||
va_end(arg_copy);
|
||||
|
||||
va_copy(arg_copy, arg);
|
||||
log_to_syslog(logLevel, newFormat.get(), arg_copy);
|
||||
va_end(arg_copy);
|
||||
|
||||
log_to_ble(logLevel, newFormat.get(), arg);
|
||||
|
||||
va_end(arg);
|
||||
#ifdef HAS_FREE_RTOS
|
||||
@@ -352,7 +342,6 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
#endif
|
||||
}
|
||||
|
||||
delete[] newFormat;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ void consoleInit()
|
||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||
// onReceive does only exist for HardwareSerial not for USB CDC serial
|
||||
Port.onReceive([sc]() { sc->rxInt(); });
|
||||
#else
|
||||
(void)sc;
|
||||
#endif
|
||||
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
||||
}
|
||||
|
||||
@@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit
|
||||
|
||||
void NotifiedWorkerThread::checkNotification()
|
||||
{
|
||||
auto n = notification;
|
||||
notification = 0; // clear notification
|
||||
// Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification
|
||||
// after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes
|
||||
// in.)
|
||||
auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe
|
||||
if (n) {
|
||||
onNotify(n);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "OSThread.h"
|
||||
#include <atomic>
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
@@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread
|
||||
/**
|
||||
* The notification that was most recently used to wake the thread. Read from runOnce()
|
||||
*/
|
||||
uint32_t notification = 0;
|
||||
std::atomic<uint32_t> notification{0};
|
||||
|
||||
public:
|
||||
NotifiedWorkerThread(const char *name) : OSThread(name) {}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
namespace concurrency
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Periodically invoke a callback. This just provides C-style callback conventions
|
||||
* rather than a virtual function - FIXME, remove?
|
||||
* @brief Periodically invoke a callback.
|
||||
* Supports both legacy function pointers and modern callables.
|
||||
*/
|
||||
class Periodic : public OSThread
|
||||
{
|
||||
int32_t (*callback)();
|
||||
|
||||
public:
|
||||
// callback returns the period for the next callback invocation (or 0 if we should no longer be called)
|
||||
Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {}
|
||||
Periodic(const char *name, int32_t (*cb)()) : OSThread(name), callback(cb) {}
|
||||
Periodic(const char *name, std::function<int32_t()> cb) : OSThread(name), callback(std::move(cb)) {}
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override { return callback(); }
|
||||
int32_t runOnce() override { return callback ? callback() : 0; }
|
||||
|
||||
private:
|
||||
std::function<int32_t()> callback;
|
||||
};
|
||||
|
||||
} // namespace concurrency
|
||||
|
||||
@@ -149,6 +149,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
|
||||
#endif
|
||||
|
||||
#ifdef USE_KCT8103L_PA
|
||||
// Power Amps are often non-linear, so we can use an array of values for the power curve
|
||||
#define NUM_PA_POINTS 22
|
||||
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7
|
||||
#endif
|
||||
|
||||
#ifdef RAK13302
|
||||
#define NUM_PA_POINTS 22
|
||||
#define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8
|
||||
@@ -163,6 +169,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define TX_GAIN_LORA 0
|
||||
#endif
|
||||
|
||||
#ifndef HAS_LORA_FEM
|
||||
#define HAS_LORA_FEM 0
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Feature toggles
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -244,6 +254,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BQ25896_ADDR 0x6B
|
||||
#define LTR553ALS_ADDR 0x23
|
||||
#define SEN5X_ADDR 0x69
|
||||
#define SCD30_ADDR 0x61
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ACCELEROMETER
|
||||
@@ -260,6 +271,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define BHI260AP_ADDR 0x28
|
||||
#define BMM150_ADDR 0x13
|
||||
#define DA217_ADDR 0x26
|
||||
#define BMI270_ADDR 0x68
|
||||
#define BMI270_ADDR_ALT 0x69
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// LED
|
||||
|
||||
@@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150};
|
||||
return firstOfOrNONE(9, types);
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
|
||||
return firstOfOrNONE(10, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
|
||||
@@ -89,9 +89,12 @@ class ScanI2C
|
||||
DA217,
|
||||
CHSC6X,
|
||||
CST226SE,
|
||||
BMI270,
|
||||
SEN5X,
|
||||
SFA30,
|
||||
CW2015
|
||||
CW2015,
|
||||
SCD30,
|
||||
ADS1115
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -117,6 +117,25 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
|
||||
{
|
||||
TwoWire *i2cBus = fetchI2CBus(addr);
|
||||
i2cBus->beginTransmission(addr.address);
|
||||
if (command > 0xFF) {
|
||||
i2cBus->write((uint8_t)(command >> 8));
|
||||
}
|
||||
i2cBus->write((uint8_t)(command & 0xFF));
|
||||
if (i2cBus->endTransmission() != 0) {
|
||||
return false;
|
||||
}
|
||||
delay(20);
|
||||
uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
|
||||
bool match = (received == expectedLength);
|
||||
while (i2cBus->available())
|
||||
i2cBus->read();
|
||||
return match;
|
||||
}
|
||||
|
||||
/// for SEN5X detection
|
||||
// Note, this code needs to be called before setting the I2C bus speed
|
||||
// for the screen at high speed. The speed needs to be at 100kHz, otherwise
|
||||
@@ -432,8 +451,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||
type = OPT3001;
|
||||
logFoundDevice("OPT3001", (uint8_t)addr.address);
|
||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) !=
|
||||
0) { // unique SHT4x serial number (6 bytes inc. CRC)
|
||||
} else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC)
|
||||
type = SHT4X;
|
||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||
} else {
|
||||
@@ -458,13 +476,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
break;
|
||||
|
||||
case LPS22HB_ADDR_ALT:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD060), 48); // get device marking
|
||||
if (registerValue != 0) {
|
||||
// SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
|
||||
if (i2cCommandResponseLength(addr, 0xD060, 48)) {
|
||||
type = SFA30;
|
||||
logFoundDevice("SFA30", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
// TODO - What happens with these two?
|
||||
// Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
|
||||
if (registerValue == 0xB1) {
|
||||
type = LPS22HB;
|
||||
logFoundDevice("LPS22HB", (uint8_t)addr.address);
|
||||
}
|
||||
break;
|
||||
SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
|
||||
SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address)
|
||||
|
||||
@@ -557,6 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
|
||||
case CST328_ADDR:
|
||||
// Do we have the CST328 or the CST226SE
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
||||
@@ -627,9 +652,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
}
|
||||
break;
|
||||
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
|
||||
// ICM20948 Register check
|
||||
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR
|
||||
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
|
||||
#ifdef HAS_ICM20948
|
||||
type = ICM20948;
|
||||
@@ -640,6 +664,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = ICM20948;
|
||||
logFoundDevice("ICM20948", (uint8_t)addr.address);
|
||||
break;
|
||||
} else if (registerValue == 0x24) {
|
||||
type = BMI270;
|
||||
logFoundDevice("BMI270", (uint8_t)addr.address);
|
||||
break;
|
||||
} else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00
|
||||
type = BMX160;
|
||||
logFoundDevice("BMX160", (uint8_t)addr.address);
|
||||
break;
|
||||
} else {
|
||||
String prod = "";
|
||||
prod = readSEN5xProductName(i2cBus, addr.address);
|
||||
@@ -693,11 +725,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
if (len == 5 && memcmp(expectedInfo, info, len) == 0) {
|
||||
LOG_INFO("NXP SE050 crypto chip found");
|
||||
type = NXP_SE050;
|
||||
|
||||
} else {
|
||||
LOG_INFO("FT6336U touchscreen found");
|
||||
type = FT6336U;
|
||||
break;
|
||||
}
|
||||
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2);
|
||||
if (registerValue == 0x8583 || registerValue == 0x8580) {
|
||||
type = ADS1115;
|
||||
logFoundDevice("ADS1115 ADC", (uint8_t)addr.address);
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_INFO("FT6336U touchscreen found");
|
||||
type = FT6336U;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C
|
||||
|
||||
uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const;
|
||||
|
||||
bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const;
|
||||
|
||||
DeviceType probeOLED(ScanI2C::DeviceAddress) const;
|
||||
|
||||
static void logFoundDevice(const char *device, uint8_t address);
|
||||
|
||||
+16
-18
@@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT;
|
||||
HardwareSerial *GPS::_serial_gps = nullptr;
|
||||
#endif
|
||||
|
||||
GPS *gps = nullptr;
|
||||
std::unique_ptr<GPS> gps = nullptr;
|
||||
|
||||
static GPSUpdateScheduling scheduling;
|
||||
|
||||
@@ -93,7 +93,7 @@ static const char *getGPSPowerStateString(GPSPowerState state)
|
||||
|
||||
#ifdef PIN_GPS_SWITCH
|
||||
// If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping
|
||||
// idefinitely
|
||||
// indefinitely
|
||||
|
||||
int lastState = LOW;
|
||||
bool firstrun = true;
|
||||
@@ -127,7 +127,7 @@ static int32_t gpsSwitch()
|
||||
return 1000;
|
||||
}
|
||||
|
||||
static concurrency::Periodic *gpsPeriodic;
|
||||
static std::unique_ptr<concurrency::Periodic> gpsPeriodic;
|
||||
#endif
|
||||
|
||||
static void UBXChecksum(uint8_t *message, size_t length)
|
||||
@@ -586,14 +586,14 @@ bool GPS::setup()
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
delay(250);
|
||||
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
||||
// Set the intial configuration of the device - these _should_ work for most AT6558 devices
|
||||
// Set the initial configuration of the device - these _should_ work for most AT6558 devices
|
||||
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Config");
|
||||
}
|
||||
|
||||
// Set the update frequence to 1Hz
|
||||
// Set the update frequency to 1Hz
|
||||
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
||||
@@ -700,7 +700,7 @@ bool GPS::setup()
|
||||
} else { // 8,9
|
||||
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
|
||||
}
|
||||
// Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next
|
||||
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
|
||||
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
|
||||
delay(1000);
|
||||
}
|
||||
@@ -733,7 +733,7 @@ bool GPS::setup()
|
||||
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||
|
||||
// For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats.
|
||||
// For M8 we want to enable NMEA version 4.10 so we can see the additional sats.
|
||||
if (gnssModel == GNSS_MODEL_UBLOX8) {
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
||||
@@ -1211,7 +1211,7 @@ int32_t GPS::runOnce()
|
||||
return disable(); // This should trigger when we have a fixed position, and get that first position
|
||||
|
||||
// 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms
|
||||
// if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake.
|
||||
// if not awake we can run super infrequently (once every 5 secs?) to see if we need to wake.
|
||||
return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000;
|
||||
}
|
||||
|
||||
@@ -1485,7 +1485,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
if (bufferSize > 2048)
|
||||
bufferSize = 2048;
|
||||
|
||||
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
|
||||
auto response = std::unique_ptr<char[]>(new char[bufferSize]); // Dynamically allocate based on baud rate
|
||||
uint16_t responseLen = 0;
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < timeout) {
|
||||
@@ -1501,19 +1501,18 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
|
||||
if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
delete[] response; // Cleanup before return
|
||||
return chipInfo.driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
// Reset the response buffer for the next potential message
|
||||
responseLen = 0;
|
||||
@@ -1522,13 +1521,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
LOG_DEBUG(response.get());
|
||||
#endif
|
||||
delete[] response; // Cleanup before return
|
||||
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
|
||||
}
|
||||
|
||||
GPS *GPS::createGps()
|
||||
std::unique_ptr<GPS> GPS::createGps()
|
||||
{
|
||||
int8_t _rx_gpio = config.position.rx_gpio;
|
||||
int8_t _tx_gpio = config.position.tx_gpio;
|
||||
@@ -1553,7 +1551,7 @@ GPS *GPS::createGps()
|
||||
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
||||
return nullptr;
|
||||
|
||||
GPS *new_gps = new GPS;
|
||||
auto new_gps = std::unique_ptr<GPS>(new GPS());
|
||||
new_gps->rx_gpio = _rx_gpio;
|
||||
new_gps->tx_gpio = _tx_gpio;
|
||||
|
||||
@@ -1581,7 +1579,7 @@ GPS *GPS::createGps()
|
||||
#ifdef PIN_GPS_SWITCH
|
||||
// toggle GPS via external GPIO switch
|
||||
pinMode(PIN_GPS_SWITCH, INPUT);
|
||||
gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch);
|
||||
gpsPeriodic = std::unique_ptr<concurrency::Periodic>(new concurrency::Periodic("GPSSwitch", gpsSwitch));
|
||||
#endif
|
||||
|
||||
// Currently disabled per issue #525 (TinyGPS++ crash bug)
|
||||
|
||||
+4
-2
@@ -2,6 +2,8 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "GPSStatus.h"
|
||||
#include "GpioLogic.h"
|
||||
#include "Observer.h"
|
||||
@@ -118,7 +120,7 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
// Creates an instance of the GPS class.
|
||||
// Returns the new instance or null if the GPS is not present.
|
||||
static GPS *createGps();
|
||||
static std::unique_ptr<GPS> createGps();
|
||||
|
||||
// Wake the GPS hardware - ready for an update
|
||||
void up();
|
||||
@@ -256,5 +258,5 @@ class GPS : private concurrency::OSThread
|
||||
uint8_t fixeddelayCtr = 0;
|
||||
};
|
||||
|
||||
extern GPS *gps;
|
||||
extern std::unique_ptr<GPS> gps;
|
||||
#endif // Exclude GPS
|
||||
|
||||
@@ -12,7 +12,7 @@ GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _lon
|
||||
|
||||
GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt)
|
||||
{
|
||||
// Change decimial representation to int32_t. I.e., 12.345 becomes 123450000
|
||||
// Change decimal representation to int32_t. I.e., 12.345 becomes 123450000
|
||||
_latitude = int32_t(lat * 1e+7);
|
||||
_longitude = int32_t(lon * 1e+7);
|
||||
GeoCoord::setCoords();
|
||||
@@ -20,7 +20,7 @@ GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt)
|
||||
|
||||
GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
|
||||
{
|
||||
// Change decimial representation to int32_t. I.e., 12.345 becomes 123450000
|
||||
// Change decimal representation to int32_t. I.e., 12.345 becomes 123450000
|
||||
_latitude = int32_t(lat * 1e+7);
|
||||
_longitude = int32_t(lon * 1e+7);
|
||||
GeoCoord::setCoords();
|
||||
@@ -467,10 +467,10 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new point bassed on the passed in poin
|
||||
* Create a new point based on the passed-in point
|
||||
* Ported from http://www.edwilliams.org/avform147.htm#LL
|
||||
* @param bearing
|
||||
* The bearing in raidans
|
||||
* The bearing in radians
|
||||
* @param range_meters
|
||||
* range in meters
|
||||
* @return GeoCoord object of point at bearing and range from initial point
|
||||
@@ -593,4 +593,4 @@ double GeoCoord::toRadians(double deg)
|
||||
double GeoCoord::toDegrees(double r)
|
||||
{
|
||||
return r * 180 / PI;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -223,7 +223,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
// This delta value works on all platforms
|
||||
timeStartMsec = now;
|
||||
zeroOffsetSecs = tv->tv_sec;
|
||||
// If this platform has a setable RTC, set it
|
||||
// If this platform has a settable RTC, set it
|
||||
#ifdef RV3028_RTC
|
||||
if (rtc_found.address == RV3028_RTC) {
|
||||
Melopero_RV3028 rtc;
|
||||
@@ -312,7 +312,7 @@ const char *RtcName(RTCQuality quality)
|
||||
* @param t The time to potentially set the RTC to.
|
||||
* @return True if the RTC was set to the provided time, false otherwise.
|
||||
*/
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t)
|
||||
{
|
||||
/* Convert to unix time
|
||||
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
|
||||
@@ -402,7 +402,7 @@ time_t gm_mktime(const struct tm *tm)
|
||||
#if !MESHTASTIC_EXCLUDE_TZ
|
||||
time_t result = 0;
|
||||
|
||||
// First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch.
|
||||
// First, get us to the start of tm->year, by calculating the number of days since the Unix epoch.
|
||||
int year = 1900 + tm->tm_year; // tm_year is years since 1900
|
||||
int year_minus_one = year - 1;
|
||||
int days_before_this_year = 0;
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps;
|
||||
|
||||
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t);
|
||||
RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t);
|
||||
|
||||
/// Return a string name for the quality
|
||||
const char *RtcName(RTCQuality quality);
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ static const uint8_t _message_CAS_CFG_RATE_1HZ[] = {
|
||||
|
||||
// CFG-NAVX (0x06, 0x07)
|
||||
// Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system
|
||||
// Qwirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable
|
||||
// Quirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable
|
||||
// and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used.
|
||||
static const uint8_t _message_CAS_CFG_NAVX_CONF[] = {
|
||||
0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings
|
||||
|
||||
+6
-6
@@ -57,7 +57,7 @@ static const uint8_t _message_CFG_PM2[] PROGMEM = {
|
||||
0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11
|
||||
};
|
||||
|
||||
// Constallation setup, none required for Neo-6
|
||||
// Constellation setup, none required for Neo-6
|
||||
|
||||
// For Neo-7 GPS & SBAS
|
||||
static const uint8_t _message_GNSS_7[] = {
|
||||
@@ -157,7 +157,7 @@ static const uint8_t _message_NAVX5[] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Reserved 9
|
||||
0x00, // Reserved 10
|
||||
0x00, // Reserved 11
|
||||
0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true)
|
||||
0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true)
|
||||
0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled)
|
||||
0x00, // Reserved 12
|
||||
0x00, // Reserved 13
|
||||
@@ -185,7 +185,7 @@ static const uint8_t _message_NAVX5_8[] = {
|
||||
0x00, // Reserved 4
|
||||
0x00, 0x00, // Reserved 5
|
||||
0x00, 0x00, // Reserved 6
|
||||
0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true)
|
||||
0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true)
|
||||
0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled)
|
||||
0x00, 0x00, // Reserved 7
|
||||
0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default
|
||||
@@ -314,7 +314,7 @@ static const uint8_t _message_DISABLE_TXT_INFO[] = {
|
||||
// This command applies to M8 products
|
||||
static const uint8_t _message_PMS[] = {
|
||||
0x00, // Version (0)
|
||||
0x03, // Power setup value 3 = Agresssive 1Hz
|
||||
0x03, // Power setup value 3 = Agressive 1Hz
|
||||
0x00, 0x00, // period: not applicable, set to 0
|
||||
0x00, 0x00, // onTime: not applicable, set to 0
|
||||
0x00, 0x00 // reserved, generated by u-center
|
||||
@@ -337,7 +337,7 @@ static const uint8_t _message_SAVE_10[] = {
|
||||
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
|
||||
// BBR will survive a restart, and power off for a while, but modules with small backup
|
||||
// batteries or super caps will not retain the config for a long power off time.
|
||||
// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after
|
||||
// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast acquisition after
|
||||
// sleep
|
||||
|
||||
// VALSET Commands for M10
|
||||
@@ -462,7 +462,7 @@ Default GNSS configuration is: GPS, Galileo, BDS B1l, with QZSS and SBAS enabled
|
||||
The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need
|
||||
the PM config. Lets try without it.
|
||||
PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats.
|
||||
The defination of "Got Fix" doesn't seem to include SBAS. Much more too this...
|
||||
The definition of "Got Fix" doesn't seem to include SBAS. Much more too this...
|
||||
Even if it was, it can take minutes (up to 12.5),
|
||||
even under good sat visibility conditions to re-acquire the SBAS data.
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
||||
return true;
|
||||
}
|
||||
|
||||
// End the update process - virtual method, overriden in derived class
|
||||
// End the update process - virtual method, overridden in derived class
|
||||
void EInkDisplay::endUpdate()
|
||||
{
|
||||
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
|
||||
@@ -259,6 +259,17 @@ bool EInkDisplay::connect()
|
||||
adafruitDisplay->setRotation(3);
|
||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||
}
|
||||
#elif defined(MINI_EPAPER_S3)
|
||||
spi1 = new SPIClass(HSPI);
|
||||
spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS);
|
||||
|
||||
// Create GxEPD2 objects
|
||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
|
||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||
|
||||
// Init GxEPD2
|
||||
adafruitDisplay->init();
|
||||
adafruitDisplay->setRotation(1);
|
||||
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
|
||||
|
||||
// Detect display model, before starting SPI
|
||||
|
||||
@@ -93,7 +93,8 @@ class EInkDisplay : public OLEDDisplay
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \
|
||||
defined(MINI_EPAPER_S3)
|
||||
SPIClass *spi1 = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI
|
||||
{
|
||||
// If tracking ghost pixels, grab memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
|
||||
dirtyPixels = std::unique_ptr<uint8_t[]>(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay()
|
||||
{
|
||||
// If we were tracking ghost pixels, free the memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
delete[] dirtyPixels;
|
||||
dirtyPixels = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ void EInkDynamicDisplay::adjustRefreshCounters()
|
||||
// Trigger the display update by calling base class
|
||||
bool EInkDynamicDisplay::update()
|
||||
{
|
||||
// Detemine the refresh mode to use, and start the update
|
||||
// Determine the refresh mode to use, and start the update
|
||||
bool refreshApproved = determineMode();
|
||||
if (refreshApproved) {
|
||||
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
|
||||
@@ -317,7 +317,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
|
||||
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags);
|
||||
}
|
||||
|
||||
// Have too many fast-refreshes occured consecutively, since last full refresh?
|
||||
// Have too many fast-refreshes occurred consecutively, since last full refresh?
|
||||
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
|
||||
{
|
||||
// If a decision was already reached, don't run the check
|
||||
@@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting()
|
||||
void EInkDynamicDisplay::resetGhostPixelTracking()
|
||||
{
|
||||
// Copy the current frame into dirtyPixels[] from the display buffer
|
||||
memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||
memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||
}
|
||||
#endif // EINK_LIMIT_GHOSTING_PX
|
||||
|
||||
@@ -561,4 +561,4 @@ void EInkDynamicDisplay::awaitRefresh()
|
||||
}
|
||||
#endif // HAS_EINK_ASYNCFULL
|
||||
|
||||
#endif // USE_EINK_DYNAMICDISPLAY
|
||||
#endif // USE_EINK_DYNAMICDISPLAY
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <memory>
|
||||
|
||||
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||
|
||||
@@ -116,11 +117,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo
|
||||
// Optional - track ghosting, pixel by pixel
|
||||
// May 2024: no longer used by any display. Kept for possible future use.
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
|
||||
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
|
||||
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||
uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
|
||||
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
|
||||
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
|
||||
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
|
||||
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
|
||||
std::unique_ptr<uint8_t[]> dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
|
||||
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
|
||||
#endif
|
||||
|
||||
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#ifdef HAS_LP5562
|
||||
#include <Wire.h>
|
||||
|
||||
#include <LP5562.h>
|
||||
extern LP5562 rgbw;
|
||||
|
||||
|
||||
+15
-2
@@ -433,12 +433,15 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||
#endif
|
||||
|
||||
#if defined(MUZI_BASE)
|
||||
// some screens seem to need a kick in the pants to turn back on
|
||||
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
|
||||
dispdev->init();
|
||||
dispdev->setBrightness(brightness);
|
||||
dispdev->flipScreenVertically();
|
||||
dispdev->resetDisplay();
|
||||
#ifdef SCREEN_12V_ENABLE
|
||||
digitalWrite(SCREEN_12V_ENABLE, HIGH);
|
||||
#endif
|
||||
delay(100);
|
||||
#endif
|
||||
#if !ARCH_PORTDUINO
|
||||
@@ -462,9 +465,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
|
||||
ui->init();
|
||||
#endif
|
||||
#ifdef USE_ST7789
|
||||
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, LOW);
|
||||
#endif
|
||||
ui->init();
|
||||
#ifdef ESP_PLATFORM
|
||||
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
||||
@@ -506,8 +511,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
#ifdef USE_ST7789
|
||||
SPI1.end();
|
||||
#if defined(ARCH_ESP32)
|
||||
#ifdef VTFT_LEDA
|
||||
pinMode(VTFT_LEDA, ANALOG);
|
||||
#endif
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, ANALOG);
|
||||
#endif
|
||||
pinMode(ST7789_RESET, ANALOG);
|
||||
pinMode(ST7789_RS, ANALOG);
|
||||
pinMode(ST7789_NSS, ANALOG);
|
||||
@@ -875,6 +884,10 @@ int32_t Screen::runOnce()
|
||||
break;
|
||||
case Cmd::STOP_ALERT_FRAME:
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
// Return from one-off alert mode back to regular frames.
|
||||
if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames();
|
||||
}
|
||||
break;
|
||||
case Cmd::STOP_BOOT_SCREEN:
|
||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||
|
||||
@@ -765,7 +765,11 @@ class Screen : public concurrency::OSThread
|
||||
DebugInfo debugInfo;
|
||||
|
||||
/// Display device
|
||||
#ifdef USE_ST7789
|
||||
ST7789Spi *dispdev;
|
||||
#else
|
||||
OLEDDisplay *dispdev;
|
||||
#endif
|
||||
|
||||
/// UI helper for rendering to frames and switching between them
|
||||
OLEDDisplayUi *ui;
|
||||
|
||||
@@ -1209,8 +1209,8 @@ void TFTDisplay::display(bool fromBlank)
|
||||
bool somethingChanged = false;
|
||||
|
||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
||||
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
|
||||
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
|
||||
colorTftMesh = __builtin_bswap16(TFT_MESH);
|
||||
colorTftBlack = __builtin_bswap16(TFT_BLACK);
|
||||
|
||||
y = 0;
|
||||
while (y < displayHeight) {
|
||||
|
||||
@@ -539,7 +539,7 @@ void menuHandler::messageResponseMenu()
|
||||
// If viewing ALL chats, hide “Mute Chat”
|
||||
if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) {
|
||||
const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex();
|
||||
auto &chan = channels.getByIndex(chIndex);
|
||||
const auto &chan = channels.getByIndex(chIndex);
|
||||
|
||||
optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel";
|
||||
optionsEnumArray[options++] = MuteChannel;
|
||||
@@ -831,7 +831,7 @@ void menuHandler::messageViewModeMenu()
|
||||
// Gather unique peers
|
||||
auto dms = messageStore.getDirectMessages();
|
||||
std::vector<uint32_t> uniquePeers;
|
||||
for (auto &m : dms) {
|
||||
for (const 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);
|
||||
@@ -1397,7 +1397,7 @@ void menuHandler::manageNodeMenu()
|
||||
}
|
||||
|
||||
if (selected == Favorite) {
|
||||
auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum);
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
@@ -2292,14 +2292,13 @@ void menuHandler::wifiToggleMenu()
|
||||
void menuHandler::screenOptionsMenu()
|
||||
{
|
||||
// Check if brightness is supported
|
||||
bool hasSupportBrightness = false;
|
||||
#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
hasSupportBrightness = true;
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// TDeck Doesn't seem to support brightness at all, at least not reliably
|
||||
hasSupportBrightness = false;
|
||||
bool hasSupportBrightness = false;
|
||||
#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
bool hasSupportBrightness = true;
|
||||
#else
|
||||
bool hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
@@ -2444,7 +2443,7 @@ void menuHandler::frameTogglesMenu()
|
||||
nodelist_hopsignal,
|
||||
nodelist_distance,
|
||||
nodelist_bearings,
|
||||
gps,
|
||||
gps_position,
|
||||
lora,
|
||||
clock,
|
||||
show_favorites,
|
||||
@@ -2482,7 +2481,7 @@ void menuHandler::frameTogglesMenu()
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||
optionsEnumArray[options++] = gps;
|
||||
optionsEnumArray[options++] = gps_position;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
|
||||
@@ -2545,7 +2544,7 @@ void menuHandler::frameTogglesMenu()
|
||||
screen->toggleFrameVisibility("nodelist_bearings");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
} else if (selected == gps) {
|
||||
} else if (selected == gps_position) {
|
||||
screen->toggleFrameVisibility("gps");
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
|
||||
@@ -171,7 +171,7 @@ unsigned long getModeCycleIntervalMs()
|
||||
|
||||
int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
{
|
||||
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||
return max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||
}
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
@@ -187,13 +187,12 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
if (totalEntries <= visibleNodeRows * columns)
|
||||
return;
|
||||
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int perPage = visibleNodeRows * columns;
|
||||
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) /
|
||||
max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns)));
|
||||
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
display->setPixel(scrollbarX, thumbY + i);
|
||||
}
|
||||
@@ -556,13 +555,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
|
||||
int maxScroll = 0;
|
||||
if (perPage > 0) {
|
||||
maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
maxScroll = max(0, (totalEntries - 1) / perPage);
|
||||
}
|
||||
|
||||
if (scrollIndex > maxScroll)
|
||||
scrollIndex = maxScroll;
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
@@ -580,7 +579,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
shownCount++;
|
||||
rowCount++;
|
||||
@@ -613,13 +612,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
if (millis() - popupTime < POPUP_DURATION_MS) {
|
||||
popupTotal = totalEntries;
|
||||
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
popupStart = startIndex + 1;
|
||||
popupEnd = std::min(startIndex + perPage, totalEntries);
|
||||
popupEnd = min(startIndex + perPage, totalEntries);
|
||||
|
||||
popupPage = (scrollIndex + 1);
|
||||
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
|
||||
popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
|
||||
|
||||
@@ -43,7 +43,7 @@ InputEvent NotificationRenderer::inEvent;
|
||||
int8_t NotificationRenderer::curSelected = 0;
|
||||
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
|
||||
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
|
||||
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are selectable options
|
||||
const char **NotificationRenderer::optionsArrayPtr = nullptr;
|
||||
const int *NotificationRenderer::optionsEnumPtr = nullptr;
|
||||
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||
@@ -95,7 +95,7 @@ void NotificationRenderer::resetBanner()
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
inEvent.kbchar = 0;
|
||||
curSelected = 0;
|
||||
alertBannerOptions = 0; // last x lines are seelctable options
|
||||
alertBannerOptions = 0; // last x lines are selectable options
|
||||
optionsArrayPtr = nullptr;
|
||||
optionsEnumPtr = nullptr;
|
||||
alertBannerCallback = NULL;
|
||||
@@ -781,4 +781,4 @@ void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title,
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -22,7 +22,7 @@ class NotificationRenderer
|
||||
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||
static const char **optionsArrayPtr;
|
||||
static const int *optionsEnumPtr;
|
||||
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||
static uint8_t alertBannerOptions; // last x lines are selectable options
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
|
||||
@@ -288,7 +288,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// **********************
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -1388,6 +1389,7 @@ static int8_t lastFrameIndex = -1;
|
||||
static uint32_t lastFrameChangeTime = 0;
|
||||
constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
int currentFrame = state->currentFrame;
|
||||
|
||||
@@ -49,7 +49,7 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// Contingency only
|
||||
// - pin wasn't set
|
||||
if (pin != (uint8_t)-1) {
|
||||
if (pin != static_cast<uint8_t>(-1)) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedance - unnecessary?
|
||||
} else
|
||||
@@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::peek()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
@@ -67,7 +67,7 @@ void LatchingBacklight::peek()
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::latch()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
@@ -89,7 +89,7 @@ void LatchingBacklight::latch()
|
||||
// Suitable for ending both peek and latch
|
||||
void LatchingBacklight::off()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
assert(pin != static_cast<uint8_t>(-1));
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
|
||||
@@ -40,7 +40,7 @@ class LatchingBacklight
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
uint8_t pin = static_cast<uint8_t>(-1);
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
|
||||
@@ -37,8 +37,8 @@ class DEPG0213BNS800 : public SSD16XX
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
void finalizeUpdate() override; // Only overriden for a slight optimization
|
||||
void finalizeUpdate() override; // Only overridden for a slight optimization
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
@@ -35,8 +35,8 @@ class DEPG0290BNS800 : public SSD16XX
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
void finalizeUpdate() override; // Only overriden for a slight optimization
|
||||
void finalizeUpdate() override; // Only overridden for a slight optimization
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
@@ -19,7 +19,7 @@ void setupNicheGraphics()
|
||||
SPIClass *hspi = new SPIClass(HSPI);
|
||||
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
|
||||
|
||||
// Setup Enk driver
|
||||
// Setup EInk driver
|
||||
Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
|
||||
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||
{
|
||||
// Only render pixels if they fall within user's cropped region
|
||||
if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight))
|
||||
assignedTile->handleAppletPixel(x, y, (Color)color);
|
||||
assignedTile->handleAppletPixel(x, y, static_cast<Color>(color));
|
||||
}
|
||||
|
||||
// Link our applet to a tile
|
||||
@@ -144,6 +144,21 @@ void InkHUD::Applet::resetDrawingSpace()
|
||||
setFont(fontSmall);
|
||||
}
|
||||
|
||||
// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it
|
||||
void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured)
|
||||
{
|
||||
if (captured)
|
||||
subscribedInputs |= input;
|
||||
else
|
||||
subscribedInputs &= ~input;
|
||||
}
|
||||
|
||||
// Checks if a specific input is enabled for this applet and should be sent to it
|
||||
bool InkHUD::Applet::isInputSubscribed(InputMask input)
|
||||
{
|
||||
return (subscribedInputs & input) == input;
|
||||
}
|
||||
|
||||
// Tell InkHUD::Renderer that we want to render now
|
||||
// Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc
|
||||
// When an applet decides it has heard something important, and wants to redraw, it calls this method
|
||||
@@ -312,7 +327,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA
|
||||
}
|
||||
|
||||
// Print text, specifying the position of any edge / corner of the textbox
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va)
|
||||
{
|
||||
printAt(x, y, text.c_str(), ha, va);
|
||||
}
|
||||
@@ -334,7 +349,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont()
|
||||
|
||||
// Parse any text which might have "special characters"
|
||||
// Re-encodes UTF-8 characters to match our 8-bit encoded fonts
|
||||
std::string InkHUD::Applet::parse(std::string text)
|
||||
std::string InkHUD::Applet::parse(const std::string &text)
|
||||
{
|
||||
return getFont().decodeUTF8(text);
|
||||
}
|
||||
@@ -361,10 +376,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
|
||||
}
|
||||
|
||||
// Determine if all characters of a string are printable using the current font
|
||||
bool InkHUD::Applet::isPrintable(std::string text)
|
||||
bool InkHUD::Applet::isPrintable(const std::string &text)
|
||||
{
|
||||
// Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled
|
||||
for (char &c : text) {
|
||||
for (const char &c : text) {
|
||||
if (c == '\x1A')
|
||||
return false;
|
||||
}
|
||||
@@ -387,7 +402,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text)
|
||||
|
||||
// Gets rendered width of a string
|
||||
// Wrapper for getTextBounds
|
||||
uint16_t InkHUD::Applet::getTextWidth(std::string text)
|
||||
uint16_t InkHUD::Applet::getTextWidth(const std::string &text)
|
||||
{
|
||||
return getTextWidth(text.c_str());
|
||||
}
|
||||
@@ -435,7 +450,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num)
|
||||
|
||||
// Print text, with word wrapping
|
||||
// Avoids splitting words in half, instead moving the entire word to a new line wherever possible
|
||||
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text)
|
||||
void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text)
|
||||
{
|
||||
// Place the AdafruitGFX cursor to suit our "top" coord
|
||||
setCursor(left, top + getFont().heightAboveCursor());
|
||||
@@ -492,15 +507,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
|
||||
|
||||
// Todo: rewrite making use of AdafruitGFX native text wrapping
|
||||
char cstr[] = {0, 0};
|
||||
int16_t l, t;
|
||||
uint16_t w, h;
|
||||
int16_t bx, by;
|
||||
uint16_t bw, bh;
|
||||
for (uint16_t c = 0; c < word.length(); c++) {
|
||||
// Shove next char into a c string
|
||||
cstr[0] = word[c];
|
||||
getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h);
|
||||
getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh);
|
||||
|
||||
// Manual newline, if next character will spill beyond screen edge
|
||||
if ((l + w) > left + width)
|
||||
if ((bx + bw) > left + width)
|
||||
setCursor(left, getCursorY() + getFont().lineHeight());
|
||||
|
||||
// Print next character
|
||||
@@ -519,7 +534,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std
|
||||
|
||||
// Simulate running printWrapped, to determine how tall the block of text will be.
|
||||
// This is a wasteful way of handling things. Maybe some way to optimize in future?
|
||||
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text)
|
||||
uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text)
|
||||
{
|
||||
// Cache the current crop region
|
||||
int16_t cL = cropLeft;
|
||||
@@ -649,7 +664,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
|
||||
|
||||
// For each node in db
|
||||
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Check if heard recently, and not our own node
|
||||
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())
|
||||
@@ -702,7 +717,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters)
|
||||
}
|
||||
|
||||
// Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly
|
||||
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY)
|
||||
void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY)
|
||||
{
|
||||
// How many times to draw along x axis
|
||||
int16_t xStart;
|
||||
@@ -770,7 +785,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n)
|
||||
│ │
|
||||
└───────────────────────────────┘
|
||||
*/
|
||||
void InkHUD::Applet::drawHeader(std::string text)
|
||||
void InkHUD::Applet::drawHeader(const std::string &text)
|
||||
{
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/*
|
||||
|
||||
Base class for InkHUD applets
|
||||
Must be overriden
|
||||
Must be overridden
|
||||
|
||||
An applet is one "program" which may show info on the display.
|
||||
|
||||
@@ -89,6 +89,9 @@ class Applet : public GFX
|
||||
virtual void onForeground() {}
|
||||
virtual void onBackground() {}
|
||||
virtual void onShutdown() {}
|
||||
|
||||
// Input Events
|
||||
|
||||
virtual void onButtonShortPress() {}
|
||||
virtual void onButtonLongPress() {}
|
||||
virtual void onExitShort() {}
|
||||
@@ -100,6 +103,18 @@ class Applet : public GFX
|
||||
virtual void onFreeText(char c) {}
|
||||
virtual void onFreeTextDone() {}
|
||||
virtual void onFreeTextCancel() {}
|
||||
// List of inputs which can be subscribed to
|
||||
enum InputMask { // | No Joystick | With Joystick |
|
||||
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
|
||||
BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold |
|
||||
EXIT_SHORT = 4, // | no-op | Back Button Click |
|
||||
EXIT_LONG = 8, // | no-op | Back Button Hold |
|
||||
NAV_UP = 16, // | no-op | Joystick Up |
|
||||
NAV_DOWN = 32, // | no-op | Joystick Down |
|
||||
NAV_LEFT = 64, // | no-op | Joystick Left |
|
||||
NAV_RIGHT = 128 // | no-op | Joystick Right |
|
||||
};
|
||||
bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded.
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
@@ -121,20 +136,28 @@ class Applet : public GFX
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
|
||||
// User Input Handling
|
||||
|
||||
uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs
|
||||
void setInputsSubscribed(uint8_t input,
|
||||
bool captured); // Set if an input should be handled by applet or not, this should not be
|
||||
// overloaded. Can take multiple inputs at once if you OR/`|` them together
|
||||
|
||||
// Text
|
||||
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const std::string &text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
|
||||
void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX,
|
||||
uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping
|
||||
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||
void drawHeader(std::string text); // Draw the standard applet header
|
||||
void drawHeader(const std::string &text); // Draw the standard applet header
|
||||
|
||||
// Meshtastic Logo
|
||||
|
||||
@@ -150,9 +173,9 @@ class Applet : public GFX
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
std::string parse(std::string text); // Handle text which might contain special chars
|
||||
std::string parse(const std::string &text); // Handle text which might contain special chars
|
||||
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
|
||||
bool isPrintable(std::string); // Check for characters which the font can't print
|
||||
bool isPrintable(const std::string &text); // Check for characters which the font can't print
|
||||
|
||||
// Convenient references
|
||||
|
||||
@@ -185,4 +208,4 @@ class Applet : public GFX
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
|
||||
// Caution: signed and unsigned types
|
||||
int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset;
|
||||
if (glyphAscender > 0)
|
||||
this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender);
|
||||
this->ascenderHeight = max(this->ascenderHeight, static_cast<uint8_t>(glyphAscender));
|
||||
|
||||
int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset;
|
||||
if (glyphDescender > 0)
|
||||
this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender);
|
||||
this->descenderHeight = max(this->descenderHeight, static_cast<uint8_t>(glyphDescender));
|
||||
}
|
||||
|
||||
// Apply any manual padding to grow or shrink the line size
|
||||
@@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding
|
||||
descenderHeight += paddingBottom;
|
||||
|
||||
// Find how far the cursor advances when we "print" a space character
|
||||
spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance;
|
||||
spaceCharWidth = gfxFont->glyph[static_cast<uint8_t>(' ') - gfxFont->first].xAdvance;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords()
|
||||
|
||||
// Convert a unicode char from set of UTF-8 bytes to UTF-32
|
||||
// Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value
|
||||
uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
|
||||
uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8)
|
||||
{
|
||||
uint32_t utf32 = 0;
|
||||
|
||||
@@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
|
||||
|
||||
// Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII
|
||||
// Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars
|
||||
std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
|
||||
std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded)
|
||||
{
|
||||
// Final processed output
|
||||
std::string decoded;
|
||||
@@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
|
||||
std::string utf8Char;
|
||||
uint8_t utf8CharSize = 0;
|
||||
|
||||
for (char &c : encoded) {
|
||||
for (const char &c : encoded) {
|
||||
|
||||
// If first byte
|
||||
if (utf8Char.empty()) {
|
||||
@@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded)
|
||||
|
||||
// Re-encode a single UTF-8 character to extended ASCII
|
||||
// Target encoding depends on the font
|
||||
char InkHUD::AppletFont::applyEncoding(std::string utf8)
|
||||
char InkHUD::AppletFont::applyEncoding(const std::string &utf8)
|
||||
{
|
||||
// ##################################################### Syntactic Sugar #####################################################
|
||||
#define REMAP(in, out) \
|
||||
|
||||
@@ -30,20 +30,21 @@ class AppletFont
|
||||
};
|
||||
|
||||
AppletFont();
|
||||
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
|
||||
explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0,
|
||||
int8_t paddingBottom = 0);
|
||||
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords(); // Width of the space character
|
||||
|
||||
std::string decodeUTF8(std::string encoded);
|
||||
std::string decodeUTF8(const std::string &encoded);
|
||||
|
||||
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||
const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font
|
||||
|
||||
private:
|
||||
uint32_t toUtf32(std::string utf8);
|
||||
char applyEncoding(std::string utf8);
|
||||
uint32_t toUtf32(const std::string &utf8);
|
||||
char applyEncoding(const std::string &utf8);
|
||||
|
||||
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||
|
||||
@@ -525,7 +525,7 @@ void InkHUD::MapApplet::calculateAllMarkers()
|
||||
}
|
||||
|
||||
// Determine the conversion factor between metres, and pixels on screen
|
||||
// May be overriden by derived applet, if custom scale required (fixed map size?)
|
||||
// May be overridden by derived applet, if custom scale required (fixed map size?)
|
||||
void InkHUD::MapApplet::calculateMapScale()
|
||||
{
|
||||
// Aspect ratio of map and screen
|
||||
@@ -555,4 +555,4 @@ void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
|
||||
drawLine(x0, y1, x1, y0, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -81,25 +81,25 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
|
||||
uint8_t InkHUD::NodeListApplet::maxCards()
|
||||
{
|
||||
// Cache result. Shouldn't change during execution
|
||||
static uint8_t cards = 0;
|
||||
static uint8_t maxCardCount = 0;
|
||||
|
||||
if (!cards) {
|
||||
if (!maxCardCount) {
|
||||
const uint16_t height = Tile::maxDisplayDimension();
|
||||
|
||||
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||
// Add cards one by one, until the latest card extends below screen
|
||||
|
||||
uint16_t y = cardH; // First card: no margin above
|
||||
cards = 1;
|
||||
maxCardCount = 1;
|
||||
|
||||
while (y < height) {
|
||||
y += cardMarginH;
|
||||
y += cardH;
|
||||
cards++;
|
||||
maxCardCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
return maxCardCount;
|
||||
}
|
||||
|
||||
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
||||
@@ -120,9 +120,26 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
||||
// Draw the main node list
|
||||
// ========================
|
||||
|
||||
// Imaginary vertical line dividing left-side and right-side info
|
||||
// Long-name will crop here
|
||||
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
|
||||
// Leave a small gutter between long-name text and right-side card content
|
||||
constexpr uint8_t rightContentGap = 2;
|
||||
|
||||
// Truncate with trailing "...", sized using the current font.
|
||||
auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) {
|
||||
constexpr const char *ellipsis = "...";
|
||||
const uint16_t ellipsisW = getTextWidth(ellipsis);
|
||||
uint16_t textW = getTextWidth(text);
|
||||
if (maxWidth == 0)
|
||||
return std::string();
|
||||
if (textW <= maxWidth)
|
||||
return text;
|
||||
if (ellipsisW > maxWidth)
|
||||
return std::string();
|
||||
while (!text.empty() && (textW + ellipsisW > maxWidth)) {
|
||||
text.pop_back();
|
||||
textW = getTextWidth(text);
|
||||
}
|
||||
return text + ellipsis;
|
||||
};
|
||||
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
uint16_t cardTopY = headerDivY + padDivH;
|
||||
@@ -137,12 +154,12 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
||||
|
||||
// Gather info
|
||||
// ========================================
|
||||
NodeNum &nodeNum = card->nodeNum;
|
||||
const NodeNum &nodeNum = card->nodeNum;
|
||||
SignalStrength &signal = card->signal;
|
||||
std::string longName; // handled below
|
||||
std::string shortName; // handled below
|
||||
std::string distance; // handled below;
|
||||
uint8_t &hopsAway = card->hopsAway;
|
||||
std::string distance; // handled below
|
||||
const uint8_t &hopsAway = card->hopsAway;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
@@ -185,41 +202,49 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
||||
setFont(fontMedium);
|
||||
printAt(0, lineAY, shortName, LEFT, MIDDLE);
|
||||
|
||||
// Print the distance
|
||||
// Right-side labels and long name are rendered in small font.
|
||||
setFont(fontSmall);
|
||||
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||
uint16_t rightContentW = 0;
|
||||
|
||||
// If we have a direct connection to the node, draw the signal indicator
|
||||
// Bottom row right: distance.
|
||||
if (!distance.empty()) {
|
||||
rightContentW = std::max(rightContentW, getTextWidth(distance));
|
||||
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||
}
|
||||
|
||||
// Top row right: direct-link signal only.
|
||||
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
|
||||
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
|
||||
uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label
|
||||
uint16_t signalH = fontMedium.lineHeight() * 0.75;
|
||||
int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75);
|
||||
int16_t signalX = width() - signalW;
|
||||
rightContentW = std::max(rightContentW, signalW);
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// Otherwise, print "hops away" info, if available
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
|
||||
std::string hopString = to_string(node->hops_away);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
hopString += "s"; // Append s for "Hops", rather than "Hop"
|
||||
|
||||
} else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
|
||||
std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops");
|
||||
rightContentW = std::max(rightContentW, getTextWidth(hopString));
|
||||
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
|
||||
}
|
||||
|
||||
// Print the long name, cropping to prevent overflow onto the right-side info
|
||||
setCrop(0, 0, dividerX - 1, height());
|
||||
printAt(0, lineBY, longName, LEFT, MIDDLE);
|
||||
// Give long names as much room as possible while still avoiding right side signal and hop space
|
||||
const uint16_t longNameMaxW =
|
||||
(rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0;
|
||||
const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW);
|
||||
|
||||
// GFX effect: "hatch" the right edge of longName area
|
||||
// If a longName has been cropped, it will appear to fade out,
|
||||
// creating a soft barrier with the right-side info
|
||||
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
|
||||
const int16_t hatchWidth = fontSmall.lineHeight();
|
||||
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
|
||||
// Safety crop
|
||||
setCrop(0, cardTopY, longNameMaxW, cardH);
|
||||
printAt(0, lineBY, longNameShown, LEFT, MIDDLE);
|
||||
|
||||
resetCrop();
|
||||
|
||||
// Draw separator between cards
|
||||
const int16_t separatorY = cardTopY + cardH - 1;
|
||||
if (separatorY < height() - 1 && (card + 1) != cards.end()) {
|
||||
for (int16_t xSep = 0; xSep < width(); xSep += 2)
|
||||
drawPixel(xSep, separatorY, BLACK);
|
||||
}
|
||||
|
||||
// Prepare to draw the next card
|
||||
resetCrop();
|
||||
cardTopY += cardH;
|
||||
|
||||
// Once we've run out of screen, stop drawing cards
|
||||
@@ -288,4 +313,4 @@ void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -65,10 +65,10 @@ class NodeListApplet : public Applet, public MeshModule
|
||||
|
||||
// Card Dimensions
|
||||
// - for rendering and for maxCards calc
|
||||
uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
uint8_t cardMarginH = 1; // Gap between cards (minimal to fit more rows)
|
||||
uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
#include "./UserAppletInputExample.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onActivate()
|
||||
{
|
||||
setGrabbed(false);
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onRender(bool full)
|
||||
{
|
||||
drawHeader("Input Example");
|
||||
uint16_t headerHeight = getHeaderHeight();
|
||||
|
||||
std::string buttonName;
|
||||
if (settings->joystick.enabled)
|
||||
buttonName = "joystick center button";
|
||||
else
|
||||
buttonName = "user button";
|
||||
|
||||
std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls";
|
||||
if (!isGrabbed)
|
||||
additional = " | Control is released, long press " + buttonName + " to grab controls";
|
||||
|
||||
printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional);
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed)
|
||||
{
|
||||
isGrabbed = grabbed;
|
||||
setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT,
|
||||
grabbed); // Enables/disables grabbing all inputs
|
||||
setInputsSubscribed(BUTTON_LONG, true); // Always grab this input
|
||||
}
|
||||
|
||||
void InkHUD::UserAppletInputExampleApplet::onButtonShortPress()
|
||||
{
|
||||
lastInput = "BUTTON_SHORT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onButtonLongPress()
|
||||
{
|
||||
lastInput = "BUTTON_LONG";
|
||||
setGrabbed(!isGrabbed);
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onExitShort()
|
||||
{
|
||||
lastInput = "EXIT_SHORT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onExitLong()
|
||||
{
|
||||
lastInput = "EXIT_LONG";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavUp()
|
||||
{
|
||||
lastInput = "NAV_UP";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavDown()
|
||||
{
|
||||
lastInput = "NAV_DOWN";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavLeft()
|
||||
{
|
||||
lastInput = "NAV_LEFT";
|
||||
requestUpdate();
|
||||
}
|
||||
void InkHUD::UserAppletInputExampleApplet::onNavRight()
|
||||
{
|
||||
lastInput = "NAV_RIGHT";
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
#endif
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class UserAppletInputExampleApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onActivate() override;
|
||||
|
||||
void onRender(bool full) override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
|
||||
private:
|
||||
std::string lastInput = "None";
|
||||
bool isGrabbed = false;
|
||||
|
||||
void setGrabbed(bool grabbed);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
|
||||
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||
{
|
||||
alwaysRender = true; // render everytime the screen is updated
|
||||
alwaysRender = true; // render every time the screen is updated
|
||||
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings->optionalFeatures.batteryIcon)
|
||||
@@ -29,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status;
|
||||
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
@@ -91,4 +91,4 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/*
|
||||
|
||||
System Applet to render an on-screeen keyboard
|
||||
System Applet to render an on-screen keyboard
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ void InkHUD::LogoApplet::onRender(bool full)
|
||||
int16_t logoCY = Y(0.5 - 0.05);
|
||||
|
||||
// Invert colors if black-on-white
|
||||
// Used during shutdown, to resport display health
|
||||
// Used during shutdown, to report display health
|
||||
// Todo: handle this in InkHUD::Renderer instead
|
||||
if (inverted) {
|
||||
fillScreen(BLACK);
|
||||
@@ -186,4 +186,4 @@ int32_t InkHUD::LogoApplet::runOnce()
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1176,7 +1176,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
const meshtastic_Channel &ch = channels.getByIndex(i);
|
||||
|
||||
if (!ch.has_settings)
|
||||
continue;
|
||||
@@ -1252,7 +1252,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
case NODE_CONFIG_CHANNEL_PRECISION: {
|
||||
previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex);
|
||||
if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) {
|
||||
items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL));
|
||||
break;
|
||||
@@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress()
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
cursor = 0;
|
||||
while (cursor < items.size() && items.at(cursor).isHeader) {
|
||||
cursor++;
|
||||
}
|
||||
if (cursor >= items.size()) {
|
||||
cursorShown = false;
|
||||
cursor = 0;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
@@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp()
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
// Select the last item that isn't a header
|
||||
cursor = items.size() - 1;
|
||||
while (items.at(cursor).isHeader) {
|
||||
if (cursor == 0) {
|
||||
cursorShown = false;
|
||||
break;
|
||||
}
|
||||
cursor--;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
@@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown()
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
cursor = 0;
|
||||
while (cursor < items.size() && items.at(cursor).isHeader) {
|
||||
cursor++;
|
||||
}
|
||||
if (cursor >= items.size()) {
|
||||
cursorShown = false;
|
||||
cursor = 0;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
@@ -1759,7 +1783,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
|
||||
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||
// Get the channel, and check if it's enabled
|
||||
meshtastic_Channel &channel = channels.getByIndex(i);
|
||||
const meshtastic_Channel &channel = channels.getByIndex(i);
|
||||
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
|
||||
continue;
|
||||
|
||||
@@ -1829,7 +1853,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text)
|
||||
{
|
||||
setFont(fontSmall);
|
||||
uint16_t wrapMaxH = 0;
|
||||
@@ -2004,7 +2028,7 @@ void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone
|
||||
}
|
||||
|
||||
// Free up any heap mmemory we'd used while selecting / sending canned messages
|
||||
// Free up any heap memory we'd used while selecting / sending canned messages
|
||||
void InkHUD::MenuApplet::freeCannedMessageResources()
|
||||
{
|
||||
cm.selectedMessageItem = nullptr;
|
||||
|
||||
@@ -55,7 +55,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||
std::string text); // Draw input field for free text
|
||||
const std::string &text); // Draw input field for free text
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
|
||||
@@ -228,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||
|
||||
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
|
||||
// Pick source of message
|
||||
MessageStore::Message *message =
|
||||
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
const MessageStore::Message *message =
|
||||
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "From:" : "DM: ";
|
||||
text += msgIsBroadcast ? "From:" : "DM: ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
@@ -252,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
text.clear();
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "Msg from " : "DM from ";
|
||||
text += msgIsBroadcast ? "Msg from " : "DM from ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
|
||||
@@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta
|
||||
// We'll mimic that behavior, just to keep in line with the other Statuses,
|
||||
// even though I'm not sure what the original reason for jumping through these extra hoops was.
|
||||
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
|
||||
const auto *btStatus = static_cast<const meshtastic::BluetoothStatus *>(status);
|
||||
|
||||
// When pairing begins
|
||||
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
// Store the passkey for rendering
|
||||
passkey = bluetoothStatus->getPasskey();
|
||||
passkey = btStatus->getPasskey();
|
||||
|
||||
// Show pairing screen
|
||||
bringToForeground();
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./FavoritesMapApplet.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Keep our own node available as map anchor/center; all others must be favorited.
|
||||
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
|
||||
}
|
||||
|
||||
void InkHUD::FavoritesMapApplet::onRender(bool full)
|
||||
{
|
||||
// Custom empty state text for favorites-only map.
|
||||
if (!enoughMarkers()) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the usual map applet first.
|
||||
MapApplet::onRender(full);
|
||||
|
||||
// Draw our latest "node of interest" as a special marker.
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
}
|
||||
|
||||
// Determine if we need to redraw the map, when we receive a new position packet.
|
||||
ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// If applet is not active, we shouldn't be handling any data.
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Try decode a position from the packet.
|
||||
bool hasPosition = false;
|
||||
float lat;
|
||||
float lng;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
if (position.has_latitude_i && position.has_longitude_i // Actually has position
|
||||
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
|
||||
{
|
||||
hasPosition = true;
|
||||
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
|
||||
lng = position.longitude_i * 1e-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if we didn't get a valid position.
|
||||
if (!hasPosition)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
const bool hasHopsAway = hopsAway >= 0;
|
||||
|
||||
// Determine if the position packet would change anything on-screen.
|
||||
bool somethingChanged = false;
|
||||
|
||||
// If our own position.
|
||||
if (isFromUs(&mp)) {
|
||||
// Ignore tiny local movement to reduce update spam.
|
||||
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
|
||||
somethingChanged = true;
|
||||
ourLastLat = lat;
|
||||
ourLastLng = lng;
|
||||
}
|
||||
} else {
|
||||
// For non-local packets, this applet only reacts to favorited nodes.
|
||||
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||
if (!sender || !sender->is_favorite)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check if this position is from someone different than our previous position packet.
|
||||
if (mp.from != lastFrom) {
|
||||
somethingChanged = true;
|
||||
lastFrom = mp.from;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
|
||||
// Same sender: check if position changed.
|
||||
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
|
||||
somethingChanged = true;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
}
|
||||
|
||||
// Same sender, same position: check if hops changed.
|
||||
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
|
||||
somethingChanged = true;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Plots position of favorited nodes from DB, with North facing up.
|
||||
Scaled to fit the most distant node.
|
||||
Size of marker represents hops away.
|
||||
The favorite node which most recently sent a position will be labeled.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class FavoritesMapApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
bool shouldDrawNode(meshtastic_NodeInfoLite *node) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet
|
||||
float lastLat = 0.0;
|
||||
float lastLng = 0.0;
|
||||
float lastHopsAway = 0;
|
||||
|
||||
float ourLastLat = 0.0; // Info about most recent local position
|
||||
float ourLastLng = 0.0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user