mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 23:32:34 +00:00
Compare commits
104 Commits
canned-pki
...
revert-863
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb3e52a94a | ||
|
|
43e0c35466 | ||
|
|
6e3be132f2 | ||
|
|
0aa11d810c | ||
|
|
4df6627ab1 | ||
|
|
e9590003f4 | ||
|
|
1c0c6b2736 | ||
|
|
602945f66b | ||
|
|
b86827967e | ||
|
|
b707001873 | ||
|
|
85afd706fd | ||
|
|
e76013fb60 | ||
|
|
b25797e1b3 | ||
|
|
bdb3fb1477 | ||
|
|
7eca061f01 | ||
|
|
4d86bbafe6 | ||
|
|
112b294ef6 | ||
|
|
5ba04ade2d | ||
|
|
69db3bd11c | ||
|
|
45bf2468a9 | ||
|
|
ce2e08e0d8 | ||
|
|
a579a9d011 | ||
|
|
6b55ec6603 | ||
|
|
f2400c9dc6 | ||
|
|
0a13bcaabf | ||
|
|
3ed831b8a3 | ||
|
|
cf716fe5ef | ||
|
|
597fa0b382 | ||
|
|
b5b9dc310f | ||
|
|
a7796fc7b4 | ||
|
|
bca0e1abde | ||
|
|
c46abe125c | ||
|
|
16b1280804 | ||
|
|
d00fda2f4d | ||
|
|
756efa7f00 | ||
|
|
0dfa11a909 | ||
|
|
c330bfe848 | ||
|
|
7d3e529b2f | ||
|
|
dd51de85f3 | ||
|
|
580fa292ac | ||
|
|
664d17c519 | ||
|
|
95d3ecb239 | ||
|
|
799cf0e8b3 | ||
|
|
35fa418739 | ||
|
|
585d9d36a8 | ||
|
|
b682ab3967 | ||
|
|
49b9d5151d | ||
|
|
3f8707cafe | ||
|
|
39780656ef | ||
|
|
153cf65214 | ||
|
|
07d354fa02 | ||
|
|
f4e93b4a2d | ||
|
|
18c4956aba | ||
|
|
15ee1c2819 | ||
|
|
f4ff210311 | ||
|
|
26747038bb | ||
|
|
8e082686a3 | ||
|
|
871986d183 | ||
|
|
821d8aa15d | ||
|
|
c4656dacf7 | ||
|
|
64d92679d0 | ||
|
|
b5aa16bade | ||
|
|
c740550d16 | ||
|
|
cb3ce1b1a8 | ||
|
|
2ad52812c0 | ||
|
|
5b9563a357 | ||
|
|
126954c2ed | ||
|
|
f6eede8597 | ||
|
|
6f2241751e | ||
|
|
1d4134c08c | ||
|
|
ffb168be00 | ||
|
|
27dc3be14a | ||
|
|
f2a63faddd | ||
|
|
d2403437ff | ||
|
|
05c176c16a | ||
|
|
7afc6ef833 | ||
|
|
68e739359f | ||
|
|
2357ea0042 | ||
|
|
cbdbaf62f1 | ||
|
|
e1c259ae32 | ||
|
|
b4dea63f44 | ||
|
|
30022c9377 | ||
|
|
0283e0658b | ||
|
|
4f142e6766 | ||
|
|
af8407aca9 | ||
|
|
e5d67310d6 | ||
|
|
47df9d4a79 | ||
|
|
4df79374b0 | ||
|
|
0bfc342b48 | ||
|
|
d9905f3c31 | ||
|
|
ee2ed0a8fb | ||
|
|
acab814b6f | ||
|
|
073c35c782 | ||
|
|
32ebc70bca | ||
|
|
5953b4704e | ||
|
|
a34c584028 | ||
|
|
865b46ceef | ||
|
|
ec5a54c523 | ||
|
|
858e8c6fef | ||
|
|
a6df18e60a | ||
|
|
034d2dd025 | ||
|
|
b8bfed2810 | ||
|
|
910fe911f8 | ||
|
|
9ab9650248 |
@@ -1,7 +1,7 @@
|
||||
# trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue
|
||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12
|
||||
FROM mcr.microsoft.com/devcontainers/cpp:2-debian-12
|
||||
|
||||
USER root
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"installTools": true,
|
||||
"version": "latest"
|
||||
"version": "3.14"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
|
||||
2
.github/actions/build-variant/action.yml
vendored
2
.github/actions/build-variant/action.yml
vendored
@@ -100,7 +100,7 @@ runs:
|
||||
id: version
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
|
||||
2
.github/workflows/build_debian_src.yml
vendored
2
.github/workflows/build_debian_src.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
PKG_VERSION: ${{ steps.version.outputs.deb }}
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
overwrite: true
|
||||
|
||||
2
.github/workflows/build_firmware.yml
vendored
2
.github/workflows/build_firmware.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
id: upload
|
||||
with:
|
||||
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
|
||||
|
||||
8
.github/workflows/build_one_arch.yml
vendored
8
.github/workflows/build_one_arch.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{inputs.arch}}-*
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
|
||||
8
.github/workflows/build_one_target.yml
vendored
8
.github/workflows/build_one_target.yml
vendored
@@ -119,7 +119,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-*-*
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: firmware-*-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -167,7 +167,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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
|
||||
18
.github/workflows/main_matrix.yml
vendored
18
.github/workflows/main_matrix.yml
vendored
@@ -168,7 +168,7 @@ jobs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -216,7 +216,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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
@@ -261,14 +261,14 @@ jobs:
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
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@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
@@ -373,7 +373,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
18
.github/workflows/merge_queue.yml
vendored
18
.github/workflows/merge_queue.yml
vendored
@@ -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@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
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@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
@@ -240,14 +240,14 @@ jobs:
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
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@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
|
||||
2
.github/workflows/package_obs.yml
vendored
2
.github/workflows/package_obs.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
2
.github/workflows/package_pio_deps.yml
vendored
2
.github/workflows/package_pio_deps.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
PLATFORMIO_CORE_DIR: pio/core
|
||||
|
||||
- name: Store binaries as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
|
||||
overwrite: true
|
||||
|
||||
2
.github/workflows/package_ppa.yml
vendored
2
.github/workflows/package_ppa.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||
merge-multiple: true
|
||||
|
||||
2
.github/workflows/pr_tests.yml
vendored
2
.github/workflows/pr_tests.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Download test artifacts
|
||||
if: needs.native-tests.result != 'skipped'
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
|
||||
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# step 3
|
||||
- name: save report as pipeline artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: report.sarif
|
||||
overwrite: true
|
||||
|
||||
6
.github/workflows/stale_bot.yml
vendored
6
.github/workflows/stale_bot.yml
vendored
@@ -20,5 +20,7 @@ jobs:
|
||||
uses: actions/stale@v10.1.0
|
||||
with:
|
||||
days-before-stale: 45
|
||||
exempt-issue-labels: pinned,3.0
|
||||
exempt-pr-labels: pinned,3.0
|
||||
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.
|
||||
close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened.
|
||||
exempt-issue-labels: pinned,3.0,triaged,backlog
|
||||
exempt-pr-labels: pinned,3.0,triaged,backlog
|
||||
|
||||
12
.github/workflows/test_native.yml
vendored
12
.github/workflows/test_native.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Save coverage information
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
if: always() # run this step even if previous step failed
|
||||
with:
|
||||
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}.zip
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Save test results
|
||||
if: always() # run this step even if previous step failed
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
if: always() # run this step even if previous step failed
|
||||
with:
|
||||
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}.zip
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
id: version
|
||||
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
reporter: java-junit
|
||||
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: code-coverage-report-${{ steps.version.outputs.long }}.zip
|
||||
path: code-coverage-report
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
@@ -8,15 +8,15 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.483
|
||||
- renovate@41.148.2
|
||||
- checkov@3.2.492
|
||||
- renovate@42.5.4
|
||||
- prettier@3.6.2
|
||||
- trufflehog@3.90.8
|
||||
- trufflehog@3.90.13
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.8.6
|
||||
- trivy@0.67.2
|
||||
- taplo@0.10.0
|
||||
- ruff@0.14.0
|
||||
- ruff@0.14.4
|
||||
- isort@7.0.0
|
||||
- markdownlint@0.45.0
|
||||
- oxipng@9.1.5
|
||||
@@ -26,9 +26,9 @@ lint:
|
||||
- hadolint@2.14.0
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.11.0
|
||||
- black@25.9.0
|
||||
- black@25.11.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.28.0
|
||||
- gitleaks@8.29.0
|
||||
- clang-format@16.0.3
|
||||
ignore:
|
||||
- linters: [ALL]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[portduino_base]
|
||||
platform =
|
||||
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
||||
https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip
|
||||
https://github.com/meshtastic/platform-native/archive/f566d364204416cdbf298e349213f7d551f793d9.zip
|
||||
framework = arduino
|
||||
|
||||
build_src_filter =
|
||||
|
||||
@@ -15,12 +15,12 @@ SET "LOGCOUNTER=0"
|
||||
SET "BPS_RESET=0"
|
||||
|
||||
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
|
||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv"
|
||||
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv heltec-v4"
|
||||
SET "C3=esp32c3"
|
||||
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
|
||||
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
|
||||
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator"
|
||||
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv"
|
||||
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv heltec-v4"
|
||||
|
||||
GOTO getopts
|
||||
:help
|
||||
|
||||
@@ -31,21 +31,23 @@ MUIDB_8MB=(
|
||||
"seeed-sensecap-indicator"
|
||||
)
|
||||
BIGDB_16MB=(
|
||||
"t-deck"
|
||||
"mesh-tab"
|
||||
"t-energy-s3"
|
||||
"dreamcatcher"
|
||||
"ESP32-S3-Pico"
|
||||
"m5stack-cores3"
|
||||
"station-g2"
|
||||
"t-eth-elite"
|
||||
"tlora-pager"
|
||||
"t-watch-s3"
|
||||
"elecrow-adv"
|
||||
"ESP32-S3-Pico"
|
||||
"heltec-v4"
|
||||
"m5stack-cores3"
|
||||
"mesh-tab"
|
||||
"station-g2"
|
||||
"t-deck"
|
||||
"t-energy-s3"
|
||||
"t-eth-elite"
|
||||
"t-watch-s3"
|
||||
"tlora-pager"
|
||||
)
|
||||
S3_VARIANTS=(
|
||||
"s3"
|
||||
"-v3"
|
||||
"-v4"
|
||||
"t-deck"
|
||||
"wireless-paper"
|
||||
"wireless-tracker"
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.14" date="2025-11-03">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.14</url>
|
||||
</release>
|
||||
<release version="2.7.13" date="2025-10-11">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.13</url>
|
||||
</release>
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.6
|
||||
2.6.7
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
meshtasticd (2.7.14.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.14
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Mon, 03 Nov 2025 16:11:31 +0000
|
||||
|
||||
meshtasticd (2.7.13.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.13
|
||||
|
||||
@@ -120,7 +120,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/19b7855e9a1d9deff37391659ca7194e4ef57c43.zip
|
||||
https://github.com/meshtastic/device-ui/archive/28167c67dfd13015a0b5eef1828f95fe8e3ab7c3.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
@@ -164,7 +164,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
|
||||
mprograms/QMC5883LCompass@1.2.3
|
||||
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
|
||||
dfrobot/DFRobot_RTU@1.0.3
|
||||
dfrobot/DFRobot_RTU@1.0.6
|
||||
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
|
||||
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
|
||||
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
|
||||
@@ -176,7 +176,7 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library
|
||||
adafruit/Adafruit LTR390 Library@1.1.2
|
||||
# renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075
|
||||
adafruit/Adafruit PCT2075@1.0.5
|
||||
adafruit/Adafruit PCT2075@1.0.6
|
||||
# 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
|
||||
|
||||
Submodule protobufs updated: 38638f19f8...7654db2e2d
@@ -839,8 +839,11 @@ void Power::readPowerStatus()
|
||||
|
||||
// Notify any status instances that are observing us
|
||||
const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent);
|
||||
LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(), powerStatus2.getIsCharging(),
|
||||
powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent());
|
||||
if (millis() > lastLogTime + 50 * 1000) {
|
||||
LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(),
|
||||
powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent());
|
||||
lastLogTime = millis();
|
||||
}
|
||||
newStatus.notifyObservers(&powerStatus2);
|
||||
#ifdef DEBUG_HEAP
|
||||
if (lastheap != memGet.getFreeHeap()) {
|
||||
|
||||
@@ -126,6 +126,11 @@ 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 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
|
||||
#endif
|
||||
|
||||
// Default system gain to 0 if not defined
|
||||
#ifndef TX_GAIN_LORA
|
||||
#define TX_GAIN_LORA 0
|
||||
@@ -245,6 +250,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// -----------------------------------------------------------------------------
|
||||
#define FT6336U_ADDR 0x48
|
||||
#define CST328_ADDR 0x1A
|
||||
#define CHSC6X_ADDR 0x2E
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
|
||||
|
||||
@@ -82,7 +82,10 @@ class ScanI2C
|
||||
BHI260AP,
|
||||
BMM150,
|
||||
TSL2561,
|
||||
DRV2605
|
||||
DRV2605,
|
||||
BH1750,
|
||||
DA217,
|
||||
CHSC6X
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -485,7 +485,26 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
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(CST328_ADDR, CST328, "CST328", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(LTR553ALS_ADDR, LTR553ALS, "LTR553ALS", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address);
|
||||
case LTR553ALS_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register
|
||||
if (registerValue == 0x92) { // LTR553ALS Part ID
|
||||
type = LTR553ALS;
|
||||
logFoundDevice("LTR553ALS", (uint8_t)addr.address);
|
||||
} else {
|
||||
// Test BH1750 - send power on command
|
||||
i2cBus->beginTransmission(addr.address);
|
||||
i2cBus->write(0x01); // Power On command
|
||||
uint8_t bh1750_error = i2cBus->endTransmission();
|
||||
if (bh1750_error == 0) {
|
||||
type = BH1750;
|
||||
logFoundDevice("BH1750", (uint8_t)addr.address);
|
||||
} else {
|
||||
LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||
|
||||
@@ -1659,8 +1659,12 @@ bool GPS::lookForLocation()
|
||||
|
||||
#ifndef TINYGPS_OPTION_NO_STATISTICS
|
||||
if (reader.failedChecksum() > lastChecksumFailCount) {
|
||||
LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount,
|
||||
reader.failedChecksum());
|
||||
// In a GPS_DEBUG build we want to log all of these. In production, we only care if there are many of them.
|
||||
#ifndef GPS_DEBUG
|
||||
if (reader.failedChecksum() > 4)
|
||||
#endif
|
||||
LOG_WARN("%u new GPS checksum failures, for a total of %u", reader.failedChecksum() - lastChecksumFailCount,
|
||||
reader.failedChecksum());
|
||||
lastChecksumFailCount = reader.failedChecksum();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -443,7 +443,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
if (uiconfig.screen_brightness == 1)
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
if (uiconfig.screen_brightness == 1)
|
||||
if (uiconfig.screen_brightness > 0)
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "main.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
@@ -423,3 +425,4 @@ std::string sanitizeString(const std::string &input)
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -422,7 +422,57 @@ static LGFX *tft = nullptr;
|
||||
|
||||
#elif defined(ST7789_CS)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
#ifdef HELTEC_V4_TFT
|
||||
#include "chsc6x.h"
|
||||
#include "lgfx/v1/Touch.hpp"
|
||||
namespace lgfx
|
||||
{
|
||||
inline namespace v1
|
||||
{
|
||||
class TOUCH_CHSC6X : public ITouch
|
||||
{
|
||||
public:
|
||||
TOUCH_CHSC6X(void)
|
||||
{
|
||||
_cfg.i2c_addr = TOUCH_SLAVE_ADDRESS;
|
||||
_cfg.x_min = 0;
|
||||
_cfg.x_max = 240;
|
||||
_cfg.y_min = 0;
|
||||
_cfg.y_max = 320;
|
||||
};
|
||||
|
||||
bool init(void) override
|
||||
{
|
||||
if (chsc6xTouch == nullptr) {
|
||||
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
|
||||
}
|
||||
chsc6xTouch->chsc6x_init();
|
||||
return true;
|
||||
};
|
||||
|
||||
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override
|
||||
{
|
||||
uint16_t raw_x, raw_y;
|
||||
if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) {
|
||||
tp[0].x = 320 - 1 - raw_y;
|
||||
tp[0].y = 240 - 1 - raw_x;
|
||||
tp[0].size = 1;
|
||||
tp[0].id = 1;
|
||||
return 1;
|
||||
}
|
||||
tp[0].size = 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
void wakeup(void) override{};
|
||||
void sleep(void) override{};
|
||||
|
||||
private:
|
||||
chsc6x *chsc6xTouch = nullptr;
|
||||
};
|
||||
} // namespace v1
|
||||
} // namespace lgfx
|
||||
#endif
|
||||
class LGFX : public lgfx::LGFX_Device
|
||||
{
|
||||
lgfx::Panel_ST7789 _panel_instance;
|
||||
@@ -431,6 +481,8 @@ class LGFX : public lgfx::LGFX_Device
|
||||
#if HAS_TOUCHSCREEN
|
||||
#if defined(T_WATCH_S3) || defined(ELECROW)
|
||||
lgfx::Touch_FT5x06 _touch_instance;
|
||||
#elif defined(HELTEC_V4_TFT)
|
||||
lgfx::TOUCH_CHSC6X _touch_instance;
|
||||
#else
|
||||
lgfx::Touch_GT911 _touch_instance;
|
||||
#endif
|
||||
@@ -464,9 +516,9 @@ class LGFX : public lgfx::LGFX_Device
|
||||
{ // Set the display panel control.
|
||||
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
|
||||
|
||||
cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable)
|
||||
cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable)
|
||||
cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable)
|
||||
cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable)
|
||||
cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable)
|
||||
|
||||
// The following setting values are general initial values for each panel, so please comment out any
|
||||
// unknown items and try them.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "VirtualKeyboard.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
@@ -736,3 +737,4 @@ bool VirtualKeyboard::isTimedOut() const
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
@@ -135,3 +137,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
|
||||
} // namespace CompassRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -515,7 +515,7 @@ void menuHandler::homeBaseMenu()
|
||||
}
|
||||
saveUIConfig();
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
if (uiconfig.screen_brightness == 1) {
|
||||
if (uiconfig.screen_brightness > 0) {
|
||||
uiconfig.screen_brightness = 0;
|
||||
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
||||
} else {
|
||||
@@ -784,6 +784,7 @@ void menuHandler::nodeNameLengthMenu()
|
||||
screen->runNow();
|
||||
}
|
||||
};
|
||||
bannerOptions.InitialSelected = config.display.use_long_node_name == true ? 1 : 2;
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
@@ -935,7 +936,9 @@ void menuHandler::BluetoothToggleMenu()
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 1 || selected == 2) {
|
||||
if (selected == 0)
|
||||
return;
|
||||
else if (selected != (config.bluetooth.enabled ? 1 : 2)) {
|
||||
InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ static int scrollIndex = 0;
|
||||
// Utility Functions
|
||||
// =============================
|
||||
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
const char *name = NULL;
|
||||
static char nodeName[16] = "?";
|
||||
@@ -81,6 +81,28 @@ const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
|
||||
if (config.display.use_long_node_name == true) {
|
||||
int availWidth = (SCREEN_WIDTH / 2) - 65;
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
size_t origLen = strlen(nodeName);
|
||||
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[strlen(nodeName) - 1] = '\0';
|
||||
}
|
||||
|
||||
// If we actually truncated, append "..." (ensure space remains in buffer)
|
||||
if (strlen(nodeName) < origLen) {
|
||||
size_t len = strlen(nodeName);
|
||||
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
|
||||
if (len > maxLen) {
|
||||
nodeName[maxLen] = '\0';
|
||||
len = maxLen;
|
||||
}
|
||||
strcat(nodeName, "...");
|
||||
}
|
||||
}
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
@@ -147,7 +169,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -192,7 +214,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -236,7 +258,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -331,7 +353,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -362,11 +384,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float bearingToNode = RAD_TO_DEG * bearing;
|
||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
// Shrink size by 2px
|
||||
int size = FONT_HEIGHT_SMALL - 5;
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
||||
/*
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
float halfSize = size / 2.0;
|
||||
|
||||
// Point of the arrow
|
||||
|
||||
@@ -563,6 +563,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// === Header ===
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -740,7 +741,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int yOffset = (isHighResolution) ? 0 : 5;
|
||||
std::string longNameStr;
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
|
||||
longNameStr = sanitizeString(ourNode->user.long_name);
|
||||
}
|
||||
@@ -1000,24 +1000,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
const char *displayLine = ""; // Initialize to empty string by default
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
|
||||
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
|
||||
|
||||
if (usePhoneGPS) {
|
||||
// Phone-provided GPS is active
|
||||
displayLine = "Phone GPS";
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
if (isHighResolution) {
|
||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||
imgSatellite_height, imgSatellite, display);
|
||||
} else {
|
||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||
imgSatellite);
|
||||
}
|
||||
int xOffset = (isHighResolution) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
// GPS disabled / not present
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
if (config.position.fixed_position) {
|
||||
displayLine = "Fixed GPS";
|
||||
} else {
|
||||
@@ -1108,9 +1091,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
|
||||
// === Final Row: Altitude ===
|
||||
char altitudeLine[32] = {0};
|
||||
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
? ourNode->position.altitude
|
||||
: geoCoord.getAltitude();
|
||||
int32_t alt = geoCoord.getAltitude();
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "emotes.h"
|
||||
|
||||
namespace graphics
|
||||
@@ -275,3 +277,4 @@ const unsigned char bell_icon[] PROGMEM = {
|
||||
#endif
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -13,45 +13,147 @@ void InkHUD::MapApplet::onRender()
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper: draw rounded rectangle centered at x,y
|
||||
auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) {
|
||||
int16_t x = cx - (w / 2);
|
||||
int16_t y = cy - (h / 2);
|
||||
|
||||
// center rects
|
||||
fillRect(x + r, y, w - 2 * r, h, color);
|
||||
fillRect(x, y + r, r, h - 2 * r, color);
|
||||
fillRect(x + w - r, y + r, r, h - 2 * r, color);
|
||||
|
||||
// corners
|
||||
fillCircle(x + r, y + r, r, color);
|
||||
fillCircle(x + w - r - 1, y + r, r, color);
|
||||
fillCircle(x + r, y + h - r - 1, r, color);
|
||||
fillCircle(x + w - r - 1, y + h - r - 1, r, color);
|
||||
};
|
||||
|
||||
// Find center of map
|
||||
// - latitude and longitude
|
||||
// - will be placed at X(0.5), Y(0.5)
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
|
||||
// Calculate North+East distance of each node to map center
|
||||
// - which nodes to use controlled by virtual shouldDrawNode method
|
||||
calculateAllMarkers();
|
||||
|
||||
// Set the region shown on the map
|
||||
// - default: fit all nodes, plus padding
|
||||
// - maybe overriden by derived applet
|
||||
// - getMapSize *sets* passed parameters (C-style)
|
||||
getMapSize(&widthMeters, &heightMeters);
|
||||
|
||||
// Set the metersToPx conversion value
|
||||
calculateMapScale();
|
||||
|
||||
// Special marker for own node
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
drawLabeledMarker(ourNode);
|
||||
|
||||
// Draw all markers
|
||||
// Draw all markers first
|
||||
for (Marker m : markers) {
|
||||
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
// Cross Size
|
||||
constexpr uint16_t csMin = 5;
|
||||
constexpr uint16_t csMax = 12;
|
||||
// Add white halo outline first
|
||||
constexpr int outlinePad = 1;
|
||||
int boxSize = 11;
|
||||
int radius = 2; // rounded corner radius
|
||||
|
||||
// Too many hops away
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) // Too many mops
|
||||
printAt(x, y, "!", CENTER, MIDDLE);
|
||||
else if (!m.hasHopsAway) // Unknown hops
|
||||
drawCross(x, y, csMin);
|
||||
else // The fewer hops, the larger the cross
|
||||
drawCross(x, y, map(m.hopsAway, 0, config.lora.hop_limit, csMax, csMin));
|
||||
// White halo background
|
||||
fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
|
||||
|
||||
// Draw inner box
|
||||
fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK);
|
||||
|
||||
// Text inside
|
||||
setFont(fontSmall);
|
||||
setTextColor(WHITE);
|
||||
|
||||
// Draw actual marker on top
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) {
|
||||
printAt(x + 1, y + 1, "X", CENTER, MIDDLE);
|
||||
} else if (!m.hasHopsAway) {
|
||||
printAt(x + 1, y + 1, "?", CENTER, MIDDLE);
|
||||
} else {
|
||||
char hopStr[4];
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway);
|
||||
printAt(x, y + 1, hopStr, CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
// Restore default font and color
|
||||
setFont(fontSmall);
|
||||
setTextColor(BLACK);
|
||||
}
|
||||
|
||||
// Dual map scale bars
|
||||
int16_t horizPx = width() * 0.25f;
|
||||
int16_t vertPx = height() * 0.25f;
|
||||
float horizMeters = horizPx / metersToPx;
|
||||
float vertMeters = vertPx / metersToPx;
|
||||
|
||||
auto formatDistance = [&](float meters, char *out, size_t len) {
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = meters * 3.28084f;
|
||||
if (feet < 528)
|
||||
snprintf(out, len, "%.0f ft", feet);
|
||||
else {
|
||||
float miles = feet / 5280.0f;
|
||||
snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles);
|
||||
}
|
||||
} else {
|
||||
if (meters >= 1000)
|
||||
snprintf(out, len, "%.1f km", meters / 1000.0f);
|
||||
else
|
||||
snprintf(out, len, "%.0f m", meters);
|
||||
}
|
||||
};
|
||||
|
||||
// Horizontal scale bar
|
||||
int16_t horizBarY = height() - 2;
|
||||
int16_t horizBarX = 1;
|
||||
drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK);
|
||||
drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK);
|
||||
drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK);
|
||||
|
||||
char horizLabel[32];
|
||||
formatDistance(horizMeters, horizLabel, sizeof(horizLabel));
|
||||
int16_t horizLabelW = getTextWidth(horizLabel);
|
||||
int16_t horizLabelH = getFont().lineHeight();
|
||||
int16_t horizLabelX = horizBarX + horizPx + 4;
|
||||
int16_t horizLabelY = horizBarY - horizLabelH + 1;
|
||||
fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE);
|
||||
printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM);
|
||||
|
||||
// Vertical scale bar
|
||||
int16_t vertBarX = 1;
|
||||
int16_t vertBarBottom = horizBarY;
|
||||
int16_t vertBarTop = vertBarBottom - vertPx;
|
||||
drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK);
|
||||
|
||||
char vertTopLabel[32];
|
||||
formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel));
|
||||
int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2;
|
||||
int16_t topLabelW = getTextWidth(vertTopLabel);
|
||||
int16_t topLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE);
|
||||
|
||||
char vertBottomLabel[32];
|
||||
formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel));
|
||||
int16_t bottomLabelY = vertBarBottom + 4;
|
||||
int16_t bottomLabelW = getTextWidth(vertBottomLabel);
|
||||
int16_t bottomLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
||||
|
||||
// Draw our node LAST with full white fill + outline
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
|
||||
|
||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||
|
||||
// White fill background + halo
|
||||
fillCircle(centerX, centerY, 8, WHITE); // big white base
|
||||
drawCircle(centerX, centerY, 8, WHITE); // crisp edge
|
||||
|
||||
// Black bullseye on top
|
||||
drawCircle(centerX, centerY, 6, BLACK);
|
||||
fillCircle(centerX, centerY, 2, BLACK);
|
||||
|
||||
// Crosshairs
|
||||
drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK);
|
||||
drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,110 +165,123 @@ void InkHUD::MapApplet::onRender()
|
||||
|
||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
{
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||
// - averages the x, y and z coords
|
||||
// - uses tan to find angles for lat / long degrees
|
||||
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||
// - latitude: triangle formed by z (north south),
|
||||
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's surface
|
||||
// If we have a valid position for our own node, use that as the anchor
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
*lat = ourNode->position.latitude_i * 1e-7;
|
||||
*lng = ourNode->position.longitude_i * 1e-7;
|
||||
} else {
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||
// - averages the x, y and z coords
|
||||
// - uses tan to find angles for lat / long degrees
|
||||
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||
// - latitude: triangle formed by z (north south),
|
||||
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's
|
||||
// surface
|
||||
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
}
|
||||
|
||||
// All NodeDB processed, find mean values
|
||||
xAvg /= positionCount;
|
||||
yAvg /= positionCount;
|
||||
zAvg /= positionCount;
|
||||
|
||||
// Longitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point of globe's surface)
|
||||
/*
|
||||
UK
|
||||
/-------\
|
||||
(Top View) /- -\
|
||||
/- (You) -\
|
||||
/- . -\
|
||||
/- . X -\
|
||||
Asia - ... - USA
|
||||
\- Y -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -----/
|
||||
Pacific
|
||||
|
||||
*/
|
||||
|
||||
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||
|
||||
// Latitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point on the globe's surface)
|
||||
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||
/*
|
||||
UK North
|
||||
/-------\ (Front View) /-------\
|
||||
(Top View) /- -\ /- -\
|
||||
/- (You) -\ /-(You) -\
|
||||
/- /. -\ /- . -\
|
||||
/- √X²+Y²/ . X -\ /- Z . -\
|
||||
Asia - /... - USA - ..... -
|
||||
\- Y -/ \- √X²+Y² -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -----/ \- -----/
|
||||
Pacific South
|
||||
*/
|
||||
|
||||
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||
}
|
||||
|
||||
// All NodeDB processed, find mean values
|
||||
xAvg /= positionCount;
|
||||
yAvg /= positionCount;
|
||||
zAvg /= positionCount;
|
||||
|
||||
// Longitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point of globe's surface)
|
||||
/*
|
||||
UK
|
||||
/-------\
|
||||
(Top View) /- -\
|
||||
/- (You) -\
|
||||
/- . -\
|
||||
/- . X -\
|
||||
Asia - ... - USA
|
||||
\- Y -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -----/
|
||||
Pacific
|
||||
|
||||
*/
|
||||
|
||||
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||
|
||||
// Latitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point on the globe's surface)
|
||||
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||
/*
|
||||
UK North
|
||||
/-------\ (Front View) /-------\
|
||||
(Top View) /- -\ /- -\
|
||||
/- (You) -\ /-(You) -\
|
||||
/- /. -\ /- . -\
|
||||
/- √X²+Y²/ . X -\ /- Z . -\
|
||||
Asia - /... - USA - ..... -
|
||||
\- Y -/ \- √X²+Y² -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -----/ \- -----/
|
||||
Pacific South
|
||||
*/
|
||||
|
||||
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||
// Use either our node position, or the mean fallback as the center
|
||||
latCenter = *lat;
|
||||
lngCenter = *lng;
|
||||
|
||||
// ----------------------------------------------
|
||||
// This has given us the "mean position"
|
||||
// This will be a position *somewhere* near the center of our nodes.
|
||||
// What we actually want is to place our center so that our outermost nodes end up on the border of our map.
|
||||
// The only real use of our "mean position" is to give us a reference frame:
|
||||
// which direction is east, and which is west.
|
||||
// This has given us either:
|
||||
// - our actual position (preferred), or
|
||||
// - a mean position (fallback if we had no fix)
|
||||
//
|
||||
// What we actually want is to place our center so that our outermost nodes
|
||||
// end up on the border of our map. The only real use of our "center" is to give
|
||||
// us a reference frame: which direction is east, and which is west.
|
||||
//------------------------------------------------
|
||||
|
||||
// Find furthest nodes from "mean lat long"
|
||||
// Find furthest nodes from our center
|
||||
// ========================================
|
||||
|
||||
float northernmost = latCenter;
|
||||
float southernmost = latCenter;
|
||||
float easternmost = lngCenter;
|
||||
@@ -184,14 +299,14 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
continue;
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float lat = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, lat);
|
||||
southernmost = min(southernmost, lat);
|
||||
float latNode = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, latNode);
|
||||
southernmost = min(southernmost, latNode);
|
||||
|
||||
// Longitude is trickier
|
||||
float lng = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lng - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lng - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
float lngNode = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
easternmost = max(easternmost, lngCenter + degEastward);
|
||||
else
|
||||
@@ -250,7 +365,6 @@ InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float ln
|
||||
m.hopsAway = hopsAway;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Draw a marker on the map for a node, with a shortname label, and backing box
|
||||
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
@@ -324,6 +438,18 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
textX = labelX + paddingW;
|
||||
}
|
||||
|
||||
// Prevent overlap with scale bars and their labels
|
||||
// Define a "safe zone" in the bottom-left where the scale bars and text are drawn
|
||||
constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height
|
||||
constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone
|
||||
bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth);
|
||||
|
||||
// If it overlaps, shift label upward slightly above the safe zone
|
||||
if (overlapsScale) {
|
||||
labelY = height() - safeZoneHeight - labelH - 2;
|
||||
textY = labelY + (labelH / 2);
|
||||
}
|
||||
|
||||
// Backing box
|
||||
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||
|
||||
@@ -127,6 +127,11 @@ void InkHUD::NodeListApplet::onRender()
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
uint16_t cardTopY = headerDivY + padDivH;
|
||||
|
||||
// Clean up deleted nodes before drawing
|
||||
cards.erase(
|
||||
std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }),
|
||||
cards.end());
|
||||
|
||||
// -- Each node in list --
|
||||
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||
|
||||
@@ -141,6 +146,11 @@ void InkHUD::NodeListApplet::onRender()
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
// Skip deleted nodes
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// -- Shortname --
|
||||
// Parse special chars in the short name
|
||||
// Use "?" if unknown
|
||||
@@ -188,7 +198,7 @@ void InkHUD::NodeListApplet::onRender()
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// Otherwise, print "hops away" info, if available
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN) {
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
|
||||
std::string hopString = to_string(node->hops_away);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
|
||||
@@ -709,7 +709,7 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t
|
||||
// Voltage
|
||||
float voltage = powerStatus->getBatteryVoltageMv() / 1000.0;
|
||||
char voltageStr[6]; // "XX.XV"
|
||||
sprintf(voltageStr, "%.1fV", voltage);
|
||||
sprintf(voltageStr, "%.2fV", voltage);
|
||||
printAt(colC[0], labelT, "Bat", CENTER, TOP);
|
||||
printAt(colC[0], valT, voltageStr, CENTER, TOP);
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include "InputBroker.h"
|
||||
#include "PowerFSM.h" // needed for event trigger
|
||||
#include "configuration.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
|
||||
InputBroker *inputBroker = nullptr;
|
||||
|
||||
InputBroker::InputBroker()
|
||||
{
|
||||
#ifdef HAS_FREE_RTOS
|
||||
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
inputEventQueue = xQueueCreate(5, sizeof(InputEvent));
|
||||
pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *));
|
||||
xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask);
|
||||
@@ -17,7 +19,7 @@ void InputBroker::registerSource(Observable<const InputEvent *> *source)
|
||||
this->inputEventObserver.observe(source);
|
||||
}
|
||||
|
||||
#ifdef HAS_FREE_RTOS
|
||||
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
void InputBroker::requestPollSoon(InputPollable *pollable)
|
||||
{
|
||||
if (xPortInIsrContext() == pdTRUE) {
|
||||
@@ -48,11 +50,17 @@ void InputBroker::processInputEventQueue()
|
||||
int InputBroker::handleInputEvent(const InputEvent *event)
|
||||
{
|
||||
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
|
||||
|
||||
if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule &&
|
||||
moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) {
|
||||
externalNotificationModule->stopNow();
|
||||
}
|
||||
|
||||
this->notifyObservers(event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef HAS_FREE_RTOS
|
||||
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
void InputBroker::pollSoonWorker(void *p)
|
||||
{
|
||||
InputBroker *instance = (InputBroker *)p;
|
||||
|
||||
@@ -59,7 +59,7 @@ class InputBroker : public Observable<const InputEvent *>
|
||||
InputBroker();
|
||||
void registerSource(Observable<const InputEvent *> *source);
|
||||
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
|
||||
#ifdef HAS_FREE_RTOS
|
||||
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
void requestPollSoon(InputPollable *pollable);
|
||||
void queueInputEvent(const InputEvent *event);
|
||||
void processInputEventQueue();
|
||||
@@ -69,7 +69,7 @@ class InputBroker : public Observable<const InputEvent *>
|
||||
int handleInputEvent(const InputEvent *event);
|
||||
|
||||
private:
|
||||
#ifdef HAS_FREE_RTOS
|
||||
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
QueueHandle_t inputEventQueue;
|
||||
QueueHandle_t pollSoonQueue;
|
||||
TaskHandle_t pollSoonTask;
|
||||
|
||||
@@ -57,7 +57,7 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
|
||||
{0x00, 0x00, 0x00},
|
||||
{0x00, 0x00, 0x00},
|
||||
{0x20, 0x00, 0x00},
|
||||
{0x00, 0x00, 0x00},
|
||||
{0x00, 0x00, '0'},
|
||||
{0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift
|
||||
};
|
||||
|
||||
|
||||
15
src/main.cpp
15
src/main.cpp
@@ -436,6 +436,12 @@ void setup()
|
||||
|
||||
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
|
||||
|
||||
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
|
||||
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
|
||||
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
|
||||
DEBUG_PORT.printf("Debug mute is enabled, there will be no serial output.\r\n");
|
||||
#endif
|
||||
|
||||
initDeepSleep();
|
||||
|
||||
#if defined(MODEM_POWER_EN)
|
||||
@@ -841,7 +847,14 @@ void setup()
|
||||
SPI.begin();
|
||||
}
|
||||
#elif !defined(ARCH_ESP32) // ARCH_RP2040
|
||||
#if defined(RAK3401) || defined(RAK13302)
|
||||
pinMode(WB_IO2, OUTPUT);
|
||||
digitalWrite(WB_IO2, HIGH);
|
||||
SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI);
|
||||
SPI1.begin();
|
||||
#else
|
||||
SPI.begin();
|
||||
#endif
|
||||
#else
|
||||
// ESP32
|
||||
#if defined(HW_SPI1_DEVICE)
|
||||
@@ -1582,7 +1595,7 @@ void loop()
|
||||
#endif
|
||||
|
||||
service->loop();
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS)
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
|
||||
if (inputBroker)
|
||||
inputBroker->processInputEventQueue();
|
||||
#endif
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
#define default_ringtone_nag_secs 60
|
||||
#define default_ringtone_nag_secs 15
|
||||
#endif
|
||||
|
||||
#define default_mqtt_address "mqtt.meshtastic.org"
|
||||
@@ -84,4 +84,4 @@ class Default
|
||||
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,33 +31,8 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
wasSeenRecently(p, true, nullptr, nullptr, &wasUpgraded); // Updates history; returns false when an upgrade is detected
|
||||
|
||||
// Handle hop_limit upgrade scenario for rebroadcasters
|
||||
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
|
||||
if (wasUpgraded && isRebroadcaster() && iface && p->hop_limit > 0) {
|
||||
// wasSeenRecently() reports false in upgrade cases so we handle replacement before the duplicate short-circuit
|
||||
// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
|
||||
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
|
||||
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
||||
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
|
||||
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
|
||||
p->hop_limit, dropThreshold);
|
||||
|
||||
if (nodeDB)
|
||||
nodeDB->updateFrom(*p);
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
|
||||
traceRouteModule->processUpgradedPacket(*p);
|
||||
#endif
|
||||
|
||||
perhapsRebroadcast(p);
|
||||
|
||||
// We already enqueued the improved copy, so make sure the incoming packet stops here.
|
||||
return true;
|
||||
}
|
||||
|
||||
// No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
|
||||
// delivering the same packet to applications/phone twice with different hop limits.
|
||||
seenRecently = true;
|
||||
if (wasUpgraded && perhapsHandleUpgradedPacket(p)) {
|
||||
return true; // we handled it, so stop processing
|
||||
}
|
||||
|
||||
if (seenRecently) {
|
||||
@@ -70,8 +45,10 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
if (isRepeated) {
|
||||
LOG_DEBUG("Repeated reliable tx");
|
||||
// Check if it's still in the Tx queue, if not, we have to relay it again
|
||||
if (!findInTxQueue(p->from, p->id))
|
||||
if (!findInTxQueue(p->from, p->id)) {
|
||||
reprocessPacket(p);
|
||||
perhapsRebroadcast(p);
|
||||
}
|
||||
} else {
|
||||
perhapsCancelDupe(p);
|
||||
}
|
||||
@@ -82,6 +59,40 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
return Router::shouldFilterReceived(p);
|
||||
}
|
||||
|
||||
bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
|
||||
if (isRebroadcaster() && iface && p->hop_limit > 0) {
|
||||
// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
|
||||
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
|
||||
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
||||
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
|
||||
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
|
||||
p->hop_limit, dropThreshold);
|
||||
|
||||
reprocessPacket(p);
|
||||
perhapsRebroadcast(p);
|
||||
|
||||
rxDupe++;
|
||||
// We already enqueued the improved copy, so make sure the incoming packet stops here.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (nodeDB)
|
||||
nodeDB->updateFrom(*p);
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
|
||||
traceRouteModule->processUpgradedPacket(*p);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
||||
@@ -121,41 +132,6 @@ bool FloodingRouter::isRebroadcaster()
|
||||
config.device.rebroadcast_mode != meshtastic_Config_DeviceConfig_RebroadcastMode_NONE;
|
||||
}
|
||||
|
||||
void FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) {
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
||||
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
if (shouldDecrementHopLimit(p)) {
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE flood: preserving hop_limit");
|
||||
}
|
||||
#if USERPREFS_EVENT_MODE
|
||||
if (tosend->hop_limit > 2) {
|
||||
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
|
||||
tosend->hop_start -= (tosend->hop_limit - 2);
|
||||
tosend->hop_limit = 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
tosend->next_hop = NO_NEXT_HOP_PREFERENCE; // this should already be the case, but just in case
|
||||
|
||||
LOG_INFO("Rebroadcast received floodmsg");
|
||||
// Note: we are careful to resend using the original senders node id
|
||||
send(tosend);
|
||||
} else {
|
||||
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Ignore 0 id broadcast");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c)
|
||||
{
|
||||
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
*/
|
||||
class FloodingRouter : public Router
|
||||
{
|
||||
private:
|
||||
/* Check if we should rebroadcast this packet, and do so if needed */
|
||||
void perhapsRebroadcast(const meshtastic_MeshPacket *p);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor
|
||||
@@ -59,6 +55,17 @@ class FloodingRouter : public Router
|
||||
*/
|
||||
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override;
|
||||
|
||||
/* Check if we should rebroadcast this packet, and do so if needed */
|
||||
virtual bool perhapsRebroadcast(const meshtastic_MeshPacket *p) = 0;
|
||||
|
||||
/* Check if we should handle an upgraded packet (with higher hop_limit)
|
||||
* @return true if we handled it (so stop processing)
|
||||
*/
|
||||
bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p);
|
||||
|
||||
/* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */
|
||||
void reprocessPacket(const meshtastic_MeshPacket *p);
|
||||
|
||||
// Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of
|
||||
// the same packet
|
||||
bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p);
|
||||
|
||||
@@ -43,31 +43,8 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
&wasUpgraded); // Updates history; returns false when an upgrade is detected
|
||||
|
||||
// Handle hop_limit upgrade scenario for rebroadcasters
|
||||
// isRebroadcaster() is duplicated in perhapsRelay(), but this avoids confusing log messages
|
||||
if (wasUpgraded && isRebroadcaster() && iface && p->hop_limit > 0) {
|
||||
// Upgrade detection bypasses the duplicate short-circuit so we replace the queued packet before exiting
|
||||
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
||||
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
|
||||
LOG_DEBUG("Processing upgraded packet 0x%08x for relay with hop limit %d (dropping queued < %d)", p->id, p->hop_limit,
|
||||
dropThreshold);
|
||||
|
||||
if (nodeDB)
|
||||
nodeDB->updateFrom(*p);
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
|
||||
traceRouteModule->processUpgradedPacket(*p);
|
||||
#endif
|
||||
|
||||
perhapsRelay(p);
|
||||
|
||||
// We already enqueued the improved copy, so make sure the incoming packet stops here.
|
||||
return true;
|
||||
}
|
||||
|
||||
// No queue entry was replaced by this upgraded copy, so treat it as a duplicate to avoid
|
||||
// delivering the same packet to applications/phone twice with different hop limits.
|
||||
seenRecently = true;
|
||||
if (wasUpgraded && perhapsHandleUpgradedPacket(p)) {
|
||||
return true; // we handled it, so stop processing
|
||||
}
|
||||
|
||||
if (seenRecently) {
|
||||
@@ -82,14 +59,20 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
||||
if (wasFallback) {
|
||||
LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node);
|
||||
// Check if it's still in the Tx queue, if not, we have to relay it again
|
||||
if (!findInTxQueue(p->from, p->id))
|
||||
perhapsRelay(p);
|
||||
if (!findInTxQueue(p->from, p->id)) {
|
||||
reprocessPacket(p);
|
||||
perhapsRebroadcast(p);
|
||||
}
|
||||
} else {
|
||||
bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit;
|
||||
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
|
||||
if (isRepeated) {
|
||||
if (!findInTxQueue(p->from, p->id) && !perhapsRelay(p) && isToUs(p) && p->want_ack)
|
||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||
if (!findInTxQueue(p->from, p->id)) {
|
||||
reprocessPacket(p);
|
||||
if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
|
||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||
}
|
||||
}
|
||||
} else if (!weWereNextHop) {
|
||||
perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay
|
||||
}
|
||||
@@ -107,13 +90,14 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
|
||||
(p->decoded.request_id != 0 || p->decoded.reply_id != 0);
|
||||
if (isAckorReply) {
|
||||
// Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" is
|
||||
// not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the destination
|
||||
// Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from"
|
||||
// is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the
|
||||
// destination
|
||||
if (p->from != 0) {
|
||||
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
|
||||
if (origTx) {
|
||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly
|
||||
// from the destination
|
||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
||||
// directly from the destination
|
||||
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
|
||||
bool weWereSoleRelayer = false;
|
||||
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer);
|
||||
@@ -134,34 +118,49 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
}
|
||||
}
|
||||
|
||||
perhapsRelay(p);
|
||||
perhapsRebroadcast(p);
|
||||
|
||||
// handle the packet as normal
|
||||
Router::sniffReceived(p, c);
|
||||
}
|
||||
|
||||
/* Check if we should be relaying this packet if so, do so. */
|
||||
bool NextHopRouter::perhapsRelay(const meshtastic_MeshPacket *p)
|
||||
/* Check if we should be rebroadcasting this packet if so, do so. */
|
||||
bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) {
|
||||
if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) {
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
||||
LOG_INFO("Relaying received message coming from %x", p->relay_node);
|
||||
if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) {
|
||||
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
||||
LOG_INFO("Rebroadcast received message coming from %x", p->relay_node);
|
||||
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
if (shouldDecrementHopLimit(p)) {
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
LOG_INFO("Router/CLIENT_BASE-to-favorite-router/CLIENT_BASE relay: preserving hop_limit");
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
if (shouldDecrementHopLimit(p)) {
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
||||
}
|
||||
#if USERPREFS_EVENT_MODE
|
||||
if (tosend->hop_limit > 2) {
|
||||
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
|
||||
tosend->hop_start -= (tosend->hop_limit - 2);
|
||||
tosend->hop_limit = 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (p->next_hop == NO_NEXT_HOP_PREFERENCE) {
|
||||
FloodingRouter::send(tosend);
|
||||
} else {
|
||||
NextHopRouter::send(tosend);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NextHopRouter::send(tosend);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
LOG_DEBUG("Not rebroadcasting: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
|
||||
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Ignore 0 id broadcast");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,13 +230,13 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't
|
||||
// get scheduled again. (This is the core of stopRetransmission.)
|
||||
// Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it
|
||||
// doesn't get scheduled again. (This is the core of stopRetransmission.)
|
||||
auto numErased = pending.erase(key);
|
||||
assert(numErased == 1);
|
||||
|
||||
// When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call
|
||||
// to startRetransmission.
|
||||
// When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the
|
||||
// call to startRetransmission.
|
||||
packetPool.release(p);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -148,7 +148,7 @@ class NextHopRouter : public FloodingRouter
|
||||
*/
|
||||
uint8_t getNextHop(NodeNum to, uint8_t relay_node);
|
||||
|
||||
/** Check if we should be relaying this packet if so, do so.
|
||||
* @return true if we did relay */
|
||||
bool perhapsRelay(const meshtastic_MeshPacket *p);
|
||||
/** Check if we should be rebroadcasting this packet if so, do so.
|
||||
* @return true if we did rebroadcast */
|
||||
bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override;
|
||||
};
|
||||
@@ -653,7 +653,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
||||
|
||||
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
||||
defined(ELECROW_PANEL)) && \
|
||||
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \
|
||||
HAS_TFT
|
||||
// switch BT off by default; use TFT programming mode or hotkey to enable
|
||||
config.bluetooth.enabled = false;
|
||||
@@ -978,12 +978,25 @@ void NodeDB::installDefaultChannels()
|
||||
channelFile.version = DEVICESTATE_CUR_VER;
|
||||
}
|
||||
|
||||
void NodeDB::resetNodes()
|
||||
void NodeDB::resetNodes(bool keepFavorites)
|
||||
{
|
||||
if (!config.position.fixed_position)
|
||||
clearLocalPosition();
|
||||
numMeshNodes = 1;
|
||||
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||
if (keepFavorites) {
|
||||
LOG_INFO("Clearing node database - preserving favorites");
|
||||
for (size_t i = 0; i < meshNodes->size(); i++) {
|
||||
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
||||
if (i > 0 && !node.is_favorite) {
|
||||
node = meshtastic_NodeInfoLite();
|
||||
} else {
|
||||
numMeshNodes += 1;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
LOG_INFO("Clearing node database - removing favorites");
|
||||
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||
}
|
||||
devicestate.has_rx_text_message = false;
|
||||
devicestate.has_rx_waypoint = false;
|
||||
saveNodeDatabaseToDisk();
|
||||
@@ -1632,13 +1645,32 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
// If should_ignore is set,
|
||||
// we need to clear the public key and other cruft, in addition to setting the node as ignored
|
||||
info->is_ignored = true;
|
||||
info->is_favorite = false;
|
||||
info->has_device_metrics = false;
|
||||
info->has_position = false;
|
||||
info->user.public_key.size = 0;
|
||||
info->user.public_key.bytes[0] = 0;
|
||||
} else {
|
||||
info->last_heard = getValidTime(RTCQualityNTP);
|
||||
info->is_favorite = true;
|
||||
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
||||
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
||||
*/
|
||||
|
||||
/* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed
|
||||
* nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the
|
||||
* new node as a favorite, and we leave last_heard alone (even if it's zero).
|
||||
*/
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
||||
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
||||
// without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add
|
||||
// contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set
|
||||
// last_heard to now, so that the add_contact node doesn't immediately get evicted.
|
||||
info->last_heard = getTime();
|
||||
} else {
|
||||
// Normal case: set is_favorite to prevent expiration.
|
||||
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
||||
info->is_favorite = true;
|
||||
}
|
||||
|
||||
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
||||
if (contact.manually_verified) {
|
||||
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
|
||||
@@ -229,7 +229,8 @@ class NodeDB
|
||||
*/
|
||||
size_t getNumOnlineMeshNodes(bool localOnly = false);
|
||||
|
||||
void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(), removeNodeByNum(NodeNum nodeNum);
|
||||
void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false),
|
||||
removeNodeByNum(NodeNum nodeNum);
|
||||
|
||||
bool factoryReset(bool eraseBleBonds = false);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ typedef struct PacketCacheEntry {
|
||||
uint8_t encrypted : 1; // Payload is encrypted
|
||||
uint8_t has_metadata : 1; // Payload includes PacketCacheMetadata
|
||||
uint8_t : 6; // Reserved for future use
|
||||
uint16_t : 8; // Reserved for future use
|
||||
uint8_t : 8; // Reserved for future use
|
||||
};
|
||||
};
|
||||
} PacketCacheEntry;
|
||||
|
||||
@@ -94,7 +94,6 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
|
||||
LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit,
|
||||
p->hop_limit);
|
||||
*wasUpgraded = true;
|
||||
seenRecently = false; // Allow router processing but prevent duplicate app delivery
|
||||
} else if (wasUpgraded) {
|
||||
*wasUpgraded = false; // Initialize to false if not an upgrade
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "main.h"
|
||||
#include "xmodem.h"
|
||||
|
||||
@@ -56,6 +57,9 @@ void PhoneAPI::handleStartConfig()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Allow subclasses to prepare for high-throughput config traffic
|
||||
onConfigStart();
|
||||
|
||||
// even if we were already connected - restart our state machine
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
@@ -70,9 +74,13 @@ void PhoneAPI::handleStartConfig()
|
||||
spiLock->unlock();
|
||||
LOG_DEBUG("Got %d files in manifest", filesManifest.size());
|
||||
|
||||
LOG_INFO("Start API client config");
|
||||
nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos
|
||||
nodeInfoQueue.clear();
|
||||
LOG_INFO("Start API client config millis=%u", millis());
|
||||
// Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue.
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = {};
|
||||
nodeInfoQueue.clear();
|
||||
}
|
||||
resetReadIndex();
|
||||
}
|
||||
|
||||
@@ -94,8 +102,12 @@ void PhoneAPI::close()
|
||||
onConnectionChanged(false);
|
||||
fromRadioScratch = {};
|
||||
toRadioScratch = {};
|
||||
nodeInfoForPhone = {};
|
||||
nodeInfoQueue.clear();
|
||||
// Clear cached node info under lock because NimBLE callbacks can still be draining it.
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = {};
|
||||
nodeInfoQueue.clear();
|
||||
}
|
||||
packetForPhone = NULL;
|
||||
filesManifest.clear();
|
||||
fromRadioNum = 0;
|
||||
@@ -150,6 +162,10 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
case meshtastic_ToRadio_mqttClientProxyMessage_tag:
|
||||
LOG_DEBUG("Got MqttClientProxy message");
|
||||
if (state != STATE_SEND_PACKETS) {
|
||||
LOG_WARN("Ignore MqttClientProxy message while completing config handshake");
|
||||
break;
|
||||
}
|
||||
if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled && moduleConfig.mqtt.enabled &&
|
||||
(channels.anyMqttEnabled() || moduleConfig.mqtt.map_reporting_enabled)) {
|
||||
mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage);
|
||||
@@ -241,13 +257,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
LOG_DEBUG("Send My NodeInfo");
|
||||
auto us = nodeDB->readNextMeshNode(readIndex);
|
||||
if (us) {
|
||||
nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(us);
|
||||
nodeInfoForPhone.has_hops_away = false;
|
||||
nodeInfoForPhone.is_favorite = true;
|
||||
auto info = TypeConversions::ConvertToNodeInfo(us);
|
||||
info.has_hops_away = false;
|
||||
info.is_favorite = true;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = info;
|
||||
}
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
||||
fromRadioScratch.node_info = nodeInfoForPhone;
|
||||
fromRadioScratch.node_info = info;
|
||||
// Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS
|
||||
nodeInfoForPhone.num = 0;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone.num = 0;
|
||||
}
|
||||
}
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
@@ -433,24 +456,43 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
break;
|
||||
|
||||
case STATE_SEND_OTHER_NODEINFOS: {
|
||||
LOG_DEBUG("Send known nodes");
|
||||
if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) {
|
||||
// Serve the next cached node without re-reading from the DB iterator.
|
||||
nodeInfoForPhone = nodeInfoQueue.front();
|
||||
nodeInfoQueue.pop_front();
|
||||
if (readIndex == 2) { // readIndex==2 will be true for the first non-us node
|
||||
LOG_INFO("Start sending nodeinfos millis=%u", millis());
|
||||
}
|
||||
|
||||
if (nodeInfoForPhone.num != 0) {
|
||||
meshtastic_NodeInfo infoToSend = {};
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) {
|
||||
// Serve the next cached node without re-reading from the DB iterator.
|
||||
nodeInfoForPhone = nodeInfoQueue.front();
|
||||
nodeInfoQueue.pop_front();
|
||||
}
|
||||
infoToSend = nodeInfoForPhone;
|
||||
if (infoToSend.num != 0)
|
||||
nodeInfoForPhone = {};
|
||||
}
|
||||
|
||||
if (infoToSend.num != 0) {
|
||||
// Just in case we stored a different user.id in the past, but should never happen going forward
|
||||
sprintf(nodeInfoForPhone.user.id, "!%08x", nodeInfoForPhone.num);
|
||||
LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
||||
nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
||||
sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
|
||||
|
||||
// Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only
|
||||
// uncomment if you really need to:
|
||||
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
|
||||
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
|
||||
|
||||
// Occasional progress logging. (readIndex==2 will be true for the first non-us node)
|
||||
if (readIndex == 2 || readIndex % 20 == 0) {
|
||||
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
|
||||
}
|
||||
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
|
||||
fromRadioScratch.node_info = nodeInfoForPhone;
|
||||
nodeInfoForPhone = {};
|
||||
fromRadioScratch.node_info = infoToSend;
|
||||
prefetchNodeInfos();
|
||||
} else {
|
||||
LOG_DEBUG("Done sending nodeinfo");
|
||||
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoQueue.clear();
|
||||
state = STATE_SEND_FILEMANIFEST;
|
||||
// Go ahead and send that ID right now
|
||||
@@ -531,11 +573,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
|
||||
void PhoneAPI::sendConfigComplete()
|
||||
{
|
||||
LOG_INFO("Config Send Complete");
|
||||
LOG_INFO("Config Send Complete millis=%u", millis());
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag;
|
||||
fromRadioScratch.config_complete_id = config_nonce;
|
||||
config_nonce = 0;
|
||||
state = STATE_SEND_PACKETS;
|
||||
|
||||
// Allow subclasses to know we've entered steady-state so they can lower power consumption
|
||||
onConfigComplete();
|
||||
|
||||
pauseBluetoothLogging = false;
|
||||
}
|
||||
|
||||
@@ -559,20 +605,23 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
{
|
||||
bool added = false;
|
||||
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
||||
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
||||
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
||||
if (!nextNode)
|
||||
break;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
|
||||
auto nextNode = nodeDB->readNextMeshNode(readIndex);
|
||||
if (!nextNode)
|
||||
break;
|
||||
|
||||
auto info = TypeConversions::ConvertToNodeInfo(nextNode);
|
||||
bool isUs = info.num == nodeDB->getNodeNum();
|
||||
info.hops_away = isUs ? 0 : info.hops_away;
|
||||
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
||||
info.snr = isUs ? 0 : info.snr;
|
||||
info.via_mqtt = isUs ? false : info.via_mqtt;
|
||||
info.is_favorite = info.is_favorite || isUs;
|
||||
nodeInfoQueue.push_back(info);
|
||||
added = true;
|
||||
auto info = TypeConversions::ConvertToNodeInfo(nextNode);
|
||||
bool isUs = info.num == nodeDB->getNodeNum();
|
||||
info.hops_away = isUs ? 0 : info.hops_away;
|
||||
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
||||
info.snr = isUs ? 0 : info.snr;
|
||||
info.via_mqtt = isUs ? false : info.via_mqtt;
|
||||
info.is_favorite = info.is_favorite || isUs;
|
||||
nodeInfoQueue.push_back(info);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (added)
|
||||
@@ -614,10 +663,17 @@ bool PhoneAPI::available()
|
||||
case STATE_SEND_COMPLETE_ID:
|
||||
return true;
|
||||
|
||||
case STATE_SEND_OTHER_NODEINFOS:
|
||||
if (nodeInfoQueue.empty())
|
||||
prefetchNodeInfos();
|
||||
case STATE_SEND_OTHER_NODEINFOS: {
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (nodeInfoQueue.empty()) {
|
||||
// Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it.
|
||||
goto PREFETCH_NODEINFO;
|
||||
}
|
||||
}
|
||||
return true; // Always say we have something, because we might need to advance our state machine
|
||||
PREFETCH_NODEINFO:
|
||||
prefetchNodeInfos();
|
||||
return true;
|
||||
case STATE_SEND_PACKETS: {
|
||||
if (!queueStatusPacketForPhone)
|
||||
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Observer.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshtastic/portnums.pb.h"
|
||||
#include <deque>
|
||||
@@ -84,6 +85,8 @@ class PhoneAPI
|
||||
std::deque<meshtastic_NodeInfo> nodeInfoQueue;
|
||||
// Tunable size of the node info cache so we can keep BLE reads non-blocking.
|
||||
static constexpr size_t kNodePrefetchDepth = 4;
|
||||
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
|
||||
concurrency::Lock nodeInfoMutex;
|
||||
|
||||
meshtastic_ToRadio toRadioScratch = {
|
||||
0}; // this is a static scratch object, any data must be copied elsewhere before returning
|
||||
@@ -133,6 +136,7 @@ class PhoneAPI
|
||||
bool available();
|
||||
|
||||
bool isConnected() { return state != STATE_SEND_NOTHING; }
|
||||
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
|
||||
|
||||
protected:
|
||||
/// Our fromradio packet while it is being assembled
|
||||
@@ -155,6 +159,11 @@ class PhoneAPI
|
||||
*/
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) {}
|
||||
|
||||
/// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state
|
||||
/// (i.e. BLE connection params)
|
||||
virtual void onConfigStart() {}
|
||||
virtual void onConfigComplete() {}
|
||||
|
||||
/// begin a new connection
|
||||
void handleStartConfig();
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
||||
{
|
||||
mp->rx_snr = lora->getSNR();
|
||||
mp->rx_rssi = lround(lora->getRSSI());
|
||||
LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError());
|
||||
}
|
||||
|
||||
void RF95Interface::setStandby()
|
||||
|
||||
@@ -272,10 +272,10 @@ uint32_t RadioInterface::getTxDelayMsec()
|
||||
uint8_t RadioInterface::getCWsize(float snr)
|
||||
{
|
||||
// The minimum value for a LoRa SNR
|
||||
const uint32_t SNR_MIN = -20;
|
||||
const int32_t SNR_MIN = -20;
|
||||
|
||||
// The maximum value for a LoRa SNR
|
||||
const uint32_t SNR_MAX = 10;
|
||||
const int32_t SNR_MAX = 10;
|
||||
|
||||
return map(snr, SNR_MIN, SNR_MAX, CWmin, CWmax);
|
||||
}
|
||||
|
||||
@@ -289,12 +289,7 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
// actual transmission as short as possible
|
||||
txp = txQueue.dequeue();
|
||||
assert(txp);
|
||||
bool sent = startSend(txp);
|
||||
if (sent) {
|
||||
// Packet has been sent, count it toward our TX airtime utilization.
|
||||
uint32_t xmitMsec = getPacketTime(txp);
|
||||
airTime->logAirtime(TX_LOG, xmitMsec);
|
||||
}
|
||||
startSend(txp);
|
||||
LOG_DEBUG("%d packets remain in the TX queue", txQueue.getMaxLen() - txQueue.getFree());
|
||||
}
|
||||
}
|
||||
@@ -413,6 +408,10 @@ void RadioLibInterface::completeSending()
|
||||
sendingPacket = NULL;
|
||||
|
||||
if (p) {
|
||||
// Packet has been sent, count it toward our TX airtime utilization.
|
||||
uint32_t xmitMsec = getPacketTime(p);
|
||||
airTime->logAirtime(TX_LOG, xmitMsec);
|
||||
|
||||
txGood++;
|
||||
if (!isFromUs(p))
|
||||
txRelay++;
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
|
||||
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
|
||||
|
||||
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
|
||||
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
|
||||
#elif defined(ARCH_STM32WL)
|
||||
// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically.
|
||||
// For now, make it dynamic again.
|
||||
#define MAX_PACKETS \
|
||||
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
|
||||
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
|
||||
|
||||
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
|
||||
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
|
||||
#else
|
||||
@@ -470,6 +479,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
|
||||
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
LOG_ERROR("Invalid portnum (bad psk?)!");
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
||||
LOG_WARN("Rejecting legacy DM");
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
#endif
|
||||
} else {
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
|
||||
@@ -272,8 +272,9 @@ typedef struct _meshtastic_AdminMessage {
|
||||
int32_t shutdown_seconds;
|
||||
/* Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. */
|
||||
int32_t factory_reset_config;
|
||||
/* Tell the node to reset the nodedb. */
|
||||
int32_t nodedb_reset;
|
||||
/* Tell the node to reset the nodedb.
|
||||
When true, favorites are preserved through reset. */
|
||||
bool nodedb_reset;
|
||||
};
|
||||
/* The node generates this key and sends it with any get_x_response packets.
|
||||
The client MUST include the same key with any set_x commands. Key expires after 300 seconds.
|
||||
@@ -459,7 +460,7 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulato
|
||||
X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_seconds,reboot_seconds), 97) \
|
||||
X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_seconds), 98) \
|
||||
X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \
|
||||
X(a, STATIC, ONEOF, INT32, (payload_variant,nodedb_reset,nodedb_reset), 100) \
|
||||
X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \
|
||||
X(a, STATIC, SINGULAR, BYTES, session_passkey, 101)
|
||||
#define meshtastic_AdminMessage_CALLBACK NULL
|
||||
#define meshtastic_AdminMessage_DEFAULT NULL
|
||||
|
||||
@@ -282,6 +282,12 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 = 113,
|
||||
/* LilyGo T-Watch Ultra */
|
||||
meshtastic_HardwareModel_T_WATCH_ULTRA = 114,
|
||||
/* Elecrow ThinkNode M3 */
|
||||
meshtastic_HardwareModel_THINKNODE_M3 = 115,
|
||||
/* RAK WISMESH_TAP_V2 with ESP32-S3 CPU */
|
||||
meshtastic_HardwareModel_WISMESH_TAP_V2 = 116,
|
||||
/* RAK3401 */
|
||||
meshtastic_HardwareModel_RAK3401 = 117,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
|
||||
@@ -101,7 +101,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* SEN5X PM SENSORS */
|
||||
meshtastic_TelemetrySensorType_SEN5X = 43,
|
||||
/* TSL2561 light sensor */
|
||||
meshtastic_TelemetrySensorType_TSL2561 = 44
|
||||
meshtastic_TelemetrySensorType_TSL2561 = 44,
|
||||
/* BH1750 light sensor */
|
||||
meshtastic_TelemetrySensorType_BH1750 = 45
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -438,8 +440,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer)
|
||||
|
||||
void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res)
|
||||
{
|
||||
if (webServerThread)
|
||||
webServerThread->markActivity();
|
||||
|
||||
LOG_DEBUG("webAPI handleAPIv1FromRadio");
|
||||
|
||||
@@ -391,6 +393,9 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
void handleStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
{
|
||||
if (webServerThread)
|
||||
webServerThread->markActivity();
|
||||
|
||||
// Get access to the parameters
|
||||
ResourceParameters *params = req->getParams();
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ Preferences prefs;
|
||||
using namespace httpsserver;
|
||||
#include "mesh/http/ContentHandler.h"
|
||||
|
||||
static const uint32_t ACTIVE_THRESHOLD_MS = 5000;
|
||||
static const uint32_t MEDIUM_THRESHOLD_MS = 30000;
|
||||
static const int32_t ACTIVE_INTERVAL_MS = 50;
|
||||
static const int32_t MEDIUM_INTERVAL_MS = 200;
|
||||
static const int32_t IDLE_INTERVAL_MS = 1000;
|
||||
|
||||
static SSLCert *cert;
|
||||
static HTTPSServer *secureServer;
|
||||
static HTTPServer *insecureServer;
|
||||
@@ -175,6 +181,32 @@ WebServerThread::WebServerThread() : concurrency::OSThread("WebServer")
|
||||
if (!config.network.wifi_enabled && !config.network.eth_enabled) {
|
||||
disable();
|
||||
}
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
void WebServerThread::markActivity()
|
||||
{
|
||||
lastActivityTime = millis();
|
||||
}
|
||||
|
||||
int32_t WebServerThread::getAdaptiveInterval()
|
||||
{
|
||||
uint32_t currentTime = millis();
|
||||
uint32_t timeSinceActivity;
|
||||
|
||||
if (currentTime >= lastActivityTime) {
|
||||
timeSinceActivity = currentTime - lastActivityTime;
|
||||
} else {
|
||||
timeSinceActivity = (UINT32_MAX - lastActivityTime) + currentTime + 1;
|
||||
}
|
||||
|
||||
if (timeSinceActivity < ACTIVE_THRESHOLD_MS) {
|
||||
return ACTIVE_INTERVAL_MS;
|
||||
} else if (timeSinceActivity < MEDIUM_THRESHOLD_MS) {
|
||||
return MEDIUM_INTERVAL_MS;
|
||||
} else {
|
||||
return IDLE_INTERVAL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t WebServerThread::runOnce()
|
||||
@@ -189,8 +221,7 @@ int32_t WebServerThread::runOnce()
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// Loop every 5ms.
|
||||
return (5);
|
||||
return getAdaptiveInterval();
|
||||
}
|
||||
|
||||
void initWebServer()
|
||||
|
||||
@@ -10,13 +10,17 @@ void createSSLCert();
|
||||
|
||||
class WebServerThread : private concurrency::OSThread
|
||||
{
|
||||
private:
|
||||
uint32_t lastActivityTime = 0;
|
||||
|
||||
public:
|
||||
WebServerThread();
|
||||
uint32_t requestRestart = 0;
|
||||
void markActivity();
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
int32_t getAdaptiveInterval();
|
||||
};
|
||||
|
||||
extern WebServerThread *webServerThread;
|
||||
|
||||
@@ -104,9 +104,18 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
(config.security.admin_key[2].size == 32 &&
|
||||
memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) {
|
||||
LOG_INFO("PKC admin payload with authorized sender key");
|
||||
|
||||
// Automatically favorite the node that is using the admin key
|
||||
auto remoteNode = nodeDB->getMeshNode(mp.from);
|
||||
if (remoteNode && !remoteNode->is_favorite) {
|
||||
remoteNode->is_favorite = true;
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
||||
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
||||
// without the user doing so deliberately.
|
||||
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
|
||||
} else {
|
||||
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
|
||||
remoteNode->is_favorite = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp);
|
||||
@@ -280,7 +289,12 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
case meshtastic_AdminMessage_nodedb_reset_tag: {
|
||||
disableBluetooth();
|
||||
LOG_INFO("Initiate node-db reset");
|
||||
nodeDB->resetNodes();
|
||||
// CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a
|
||||
// favorited node, so ensure that their favorites are kept on reset
|
||||
bool rolePreference =
|
||||
isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE,
|
||||
meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE);
|
||||
nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset);
|
||||
reboot(DEFAULT_REBOOT_SECONDS);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -973,8 +973,14 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
||||
LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||
|
||||
if (p->to != 0xffffffff) {
|
||||
LOG_INFO("Proactively adding %x as favorite node", p->to);
|
||||
nodeDB->set_favorite(true, p->to);
|
||||
// Only add as favorite if our role is NOT CLIENT_BASE
|
||||
if (config.device.role != 12) {
|
||||
LOG_INFO("Proactively adding %x as favorite node", p->to);
|
||||
nodeDB->set_favorite(true, p->to);
|
||||
} else {
|
||||
LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", p->to);
|
||||
}
|
||||
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
p->pki_encrypted = true;
|
||||
p->channel = 0;
|
||||
|
||||
@@ -95,22 +95,9 @@ int32_t ExternalNotificationModule::runOnce()
|
||||
isRtttlPlaying = isRtttlPlaying || audioThread->isPlaying();
|
||||
#endif
|
||||
if ((nagCycleCutoff < millis()) && !isRtttlPlaying) {
|
||||
// let the song finish if we reach timeout
|
||||
// Turn off external notification immediately when timeout is reached, regardless of song state
|
||||
nagCycleCutoff = UINT32_MAX;
|
||||
LOG_INFO("Turning off external notification: ");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
setExternalState(i, false);
|
||||
externalTurnedOn[i] = 0;
|
||||
LOG_INFO("%d ", i);
|
||||
}
|
||||
LOG_INFO("");
|
||||
#ifdef HAS_I2S
|
||||
// GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library
|
||||
// T-Deck uses GPIO0 as trackball button, so restore the mode
|
||||
#if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0)
|
||||
pinMode(0, INPUT);
|
||||
#endif
|
||||
#endif
|
||||
ExternalNotificationModule::stopNow();
|
||||
isNagging = false;
|
||||
return INT32_MAX; // save cycles till we're needed again
|
||||
}
|
||||
@@ -318,21 +305,36 @@ bool ExternalNotificationModule::nagging()
|
||||
|
||||
void ExternalNotificationModule::stopNow()
|
||||
{
|
||||
LOG_INFO("Turning off external notification: ");
|
||||
LOG_INFO("Stop RTTTL playback");
|
||||
rtttl::stop();
|
||||
#ifdef HAS_I2S
|
||||
LOG_INFO("Stop audioThread playback");
|
||||
if (audioThread->isPlaying())
|
||||
audioThread->stop();
|
||||
#endif
|
||||
nagCycleCutoff = 1; // small value
|
||||
isNagging = false;
|
||||
// Turn off all outputs
|
||||
LOG_INFO("Turning off setExternalStates: ");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
setExternalState(i, false);
|
||||
externalTurnedOn[i] = 0;
|
||||
LOG_INFO("%d ", i);
|
||||
}
|
||||
setIntervalFromNow(0);
|
||||
#ifdef T_WATCH_S3
|
||||
drv.stop();
|
||||
#endif
|
||||
|
||||
// Prevent the state machine from immediately re-triggering outputs after a manual stop.
|
||||
isNagging = false;
|
||||
nagCycleCutoff = UINT32_MAX;
|
||||
|
||||
#ifdef HAS_I2S
|
||||
// GPIO0 is used as mclk for I2S audio and set to OUTPUT by the sound library
|
||||
// T-Deck uses GPIO0 as trackball button, so restore the mode
|
||||
#if defined(T_DECK) || (defined(BUTTON_PIN) && BUTTON_PIN == 0)
|
||||
pinMode(0, INPUT);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
|
||||
LOG_DEBUG("---- Received Packet:");
|
||||
LOG_DEBUG("mp.from %d", mp.from);
|
||||
LOG_DEBUG("mp.rx_snr %f", mp.rx_snr);
|
||||
LOG_DEBUG("mp.rx_rssi %f", mp.rx_rssi);
|
||||
LOG_DEBUG("mp.hop_limit %d", mp.hop_limit);
|
||||
LOG_DEBUG("---- Node Information of Received Packet (mp.from):");
|
||||
LOG_DEBUG("n->user.long_name %s", n->user.long_name);
|
||||
@@ -234,8 +235,8 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
||||
}
|
||||
|
||||
// Print the CSV header
|
||||
if (fileToWrite.println(
|
||||
"time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")) {
|
||||
if (fileToWrite.println("time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx "
|
||||
"snr,distance,hop limit,payload,rx rssi")) {
|
||||
LOG_INFO("File was written");
|
||||
} else {
|
||||
LOG_ERROR("File write failed");
|
||||
@@ -297,6 +298,8 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
||||
|
||||
// TODO: If quotes are found in the payload, it has to be escaped.
|
||||
fileToAppend.printf("\"%s\"\n", p.payload.bytes);
|
||||
fileToAppend.printf("%i,", mp.rx_rssi); // RX RSSI
|
||||
|
||||
fileToAppend.flush();
|
||||
fileToAppend.close();
|
||||
|
||||
|
||||
@@ -204,6 +204,10 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
||||
this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi;
|
||||
this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr;
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start;
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
|
||||
this->packetHistoryTotalCount++;
|
||||
@@ -256,6 +260,10 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t
|
||||
p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji;
|
||||
p->rx_rssi = this->packetHistory[i].rx_rssi;
|
||||
p->rx_snr = this->packetHistory[i].rx_snr;
|
||||
p->hop_start = this->packetHistory[i].hop_start;
|
||||
p->hop_limit = this->packetHistory[i].hop_limit;
|
||||
p->via_mqtt = this->packetHistory[i].via_mqtt;
|
||||
p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism;
|
||||
|
||||
// Let's assume that if the server received the S&F request that the client is in range.
|
||||
// TODO: Make this configurable.
|
||||
|
||||
@@ -21,6 +21,10 @@ struct PacketHistoryStruct {
|
||||
pb_size_t payload_size;
|
||||
int32_t rx_rssi;
|
||||
float rx_snr;
|
||||
uint8_t hop_start;
|
||||
uint8_t hop_limit;
|
||||
bool via_mqtt;
|
||||
uint8_t transport_mechanism;
|
||||
};
|
||||
|
||||
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
|
||||
|
||||
@@ -85,10 +85,8 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
||||
switch (event->inputEvent) {
|
||||
// GPS
|
||||
case INPUT_BROKER_GPS_TOGGLE:
|
||||
LOG_WARN("GPS Toggle");
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
if (gps) {
|
||||
LOG_WARN("GPS Toggle2");
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
||||
config.position.fixed_position == false) {
|
||||
nodeDB->clearLocalPosition();
|
||||
|
||||
@@ -361,6 +361,7 @@ bool EnvironmentTelemetryModule::wantUIFrame()
|
||||
return moduleConfig.telemetry.environment_screen_enabled;
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// === Setup display ===
|
||||
@@ -510,6 +511,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
||||
currentY += rowHeight;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
|
||||
@@ -108,6 +108,7 @@ bool PowerTelemetryModule::wantUIFrame()
|
||||
return moduleConfig.telemetry.power_screen_enabled;
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
@@ -165,6 +166,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
||||
drawLine("Ch3", m.ch3_voltage, m.ch3_current);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,113 @@ extern graphics::Screen *screen;
|
||||
|
||||
TraceRouteModule *traceRouteModule;
|
||||
|
||||
void TraceRouteModule::setResultText(const String &text)
|
||||
{
|
||||
resultText = text;
|
||||
resultLines.clear();
|
||||
resultLinesDirty = true;
|
||||
}
|
||||
|
||||
void TraceRouteModule::clearResultLines()
|
||||
{
|
||||
resultLines.clear();
|
||||
resultLinesDirty = false;
|
||||
}
|
||||
#if HAS_SCREEN
|
||||
void TraceRouteModule::rebuildResultLines(OLEDDisplay *display)
|
||||
{
|
||||
if (!display) {
|
||||
resultLinesDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
resultLines.clear();
|
||||
|
||||
if (resultText.length() == 0) {
|
||||
resultLinesDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxWidth = display->getWidth() - 4;
|
||||
if (maxWidth <= 0) {
|
||||
resultLinesDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
int textLength = resultText.length();
|
||||
|
||||
while (start <= textLength) {
|
||||
int newlinePos = resultText.indexOf('\n', start);
|
||||
String segment;
|
||||
|
||||
if (newlinePos != -1) {
|
||||
segment = resultText.substring(start, newlinePos);
|
||||
start = newlinePos + 1;
|
||||
} else {
|
||||
segment = resultText.substring(start);
|
||||
start = textLength + 1;
|
||||
}
|
||||
|
||||
if (segment.length() == 0) {
|
||||
resultLines.push_back("");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (display->getStringWidth(segment) <= maxWidth) {
|
||||
resultLines.push_back(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
String remaining = segment;
|
||||
|
||||
while (remaining.length() > 0) {
|
||||
String tempLine = "";
|
||||
int lastGoodBreak = -1;
|
||||
bool lineComplete = false;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(remaining.length()); i++) {
|
||||
char ch = remaining.charAt(i);
|
||||
String testLine = tempLine + ch;
|
||||
|
||||
if (display->getStringWidth(testLine) > maxWidth) {
|
||||
if (lastGoodBreak >= 0) {
|
||||
resultLines.push_back(remaining.substring(0, lastGoodBreak + 1));
|
||||
remaining = remaining.substring(lastGoodBreak + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else if (tempLine.length() > 0) {
|
||||
resultLines.push_back(tempLine);
|
||||
remaining = remaining.substring(i);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else {
|
||||
resultLines.push_back(String(ch));
|
||||
remaining = remaining.substring(i + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tempLine = testLine;
|
||||
if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') {
|
||||
lastGoodBreak = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineComplete) {
|
||||
if (tempLine.length() > 0) {
|
||||
resultLines.push_back(tempLine);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultLinesDirty = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r)
|
||||
{
|
||||
// We only alter the packet in alterReceivedProtobuf()
|
||||
@@ -21,6 +128,11 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti
|
||||
{
|
||||
const meshtastic_Data &incoming = p.decoded;
|
||||
|
||||
// Update next-hops using returned route
|
||||
if (incoming.request_id) {
|
||||
updateNextHops(p, r);
|
||||
}
|
||||
|
||||
// Insert unknown hops if necessary
|
||||
insertUnknownHops(p, r, !incoming.request_id);
|
||||
|
||||
@@ -153,6 +265,65 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r)
|
||||
{
|
||||
// E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D
|
||||
// Similarly, if we are C, we can set D as next-hop for D
|
||||
// If we are A, we can set B as next-hop for B, C and D
|
||||
|
||||
// First check if we were the original sender or in the original route
|
||||
int8_t nextHopIndex = -1;
|
||||
if (isToUs(&p)) {
|
||||
nextHopIndex = 0; // We are the original sender, next hop is first in route
|
||||
} else {
|
||||
// Check if we are in the original route
|
||||
for (uint8_t i = 0; i < r->route_count; i++) {
|
||||
if (r->route[i] == nodeDB->getNodeNum()) {
|
||||
nextHopIndex = i + 1; // Next hop is the one after us
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in the original route, update the next hops
|
||||
if (nextHopIndex != -1) {
|
||||
// For every node after us, we can set the next-hop to the first node after us
|
||||
NodeNum nextHop;
|
||||
if (nextHopIndex == r->route_count) {
|
||||
nextHop = p.from; // We are the last in the route, next hop is destination
|
||||
} else {
|
||||
nextHop = r->route[nextHopIndex];
|
||||
}
|
||||
|
||||
if (nextHop == NODENUM_BROADCAST) {
|
||||
return;
|
||||
}
|
||||
uint8_t nextHopByte = nodeDB->getLastByteOfNodeNum(nextHop);
|
||||
|
||||
// For the rest of the nodes in the route, set their next-hop
|
||||
// Note: if we are the last in the route, this loop will not run
|
||||
for (int8_t i = nextHopIndex; i < r->route_count; i++) {
|
||||
NodeNum targetNode = r->route[i];
|
||||
maybeSetNextHop(targetNode, nextHopByte);
|
||||
}
|
||||
|
||||
// Also set next-hop for the destination node
|
||||
maybeSetNextHop(p.from, nextHopByte);
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte)
|
||||
{
|
||||
if (target == NODENUM_BROADCAST)
|
||||
return;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target);
|
||||
if (node && node->next_hop != nextHopByte) {
|
||||
LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte);
|
||||
node->next_hop = nextHopByte;
|
||||
}
|
||||
}
|
||||
|
||||
void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag || mp.decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP)
|
||||
@@ -342,7 +513,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
if (node == 0 || node == NODENUM_BROADCAST) {
|
||||
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Invalid node";
|
||||
setResultText("Invalid node");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -356,7 +527,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
if (node == nodeDB->getNodeNum()) {
|
||||
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Cannot trace self";
|
||||
setResultText("Cannot trace self");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -383,6 +554,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
|
||||
bannerText = String("Wait for ") + String(wait) + String("s");
|
||||
runState = TRACEROUTE_STATE_COOLDOWN;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
@@ -395,6 +568,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
tracingNode = node;
|
||||
lastTraceRouteTime = now;
|
||||
runState = TRACEROUTE_STATE_TRACKING;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
bannerText = String("Tracing ") + getNodeName(node);
|
||||
|
||||
LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node);
|
||||
@@ -437,7 +612,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
} else {
|
||||
LOG_ERROR("MeshService is NULL!");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Service unavailable";
|
||||
setResultText("Service unavailable");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -450,7 +625,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
|
||||
} else {
|
||||
LOG_ERROR("Failed to allocate TraceRoute packet from router");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Failed to send";
|
||||
setResultText("Failed to send");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -468,7 +643,7 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
if (node == 0 || node == NODENUM_BROADCAST) {
|
||||
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Invalid node";
|
||||
setResultText("Invalid node");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -482,7 +657,7 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
if (node == nodeDB->getNodeNum()) {
|
||||
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Cannot trace self";
|
||||
setResultText("Cannot trace self");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -504,6 +679,8 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
|
||||
bannerText = String("Wait for ") + String(wait) + String("s");
|
||||
runState = TRACEROUTE_STATE_COOLDOWN;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
|
||||
requestFocus();
|
||||
UIFrameEvent e;
|
||||
@@ -516,6 +693,8 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
runState = TRACEROUTE_STATE_TRACKING;
|
||||
tracingNode = node;
|
||||
lastTraceRouteTime = now;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
bannerText = String("Tracing ") + getNodeName(node);
|
||||
|
||||
requestFocus();
|
||||
@@ -550,14 +729,14 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
} else {
|
||||
LOG_ERROR("MeshService is NULL!");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Service unavailable";
|
||||
setResultText("Service unavailable");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Failed to allocate TraceRoute packet from router");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "Failed to send";
|
||||
setResultText("Failed to send");
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
}
|
||||
@@ -565,7 +744,7 @@ void TraceRouteModule::launch(NodeNum node)
|
||||
|
||||
void TraceRouteModule::handleTraceRouteResult(const String &result)
|
||||
{
|
||||
resultText = result;
|
||||
setResultText(result);
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultShowTime = millis();
|
||||
tracingNode = 0;
|
||||
@@ -615,83 +794,15 @@ void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
if (resultText.length() > 0) {
|
||||
std::vector<String> lines;
|
||||
String currentLine = "";
|
||||
int maxWidth = display->getWidth() - 4;
|
||||
|
||||
int start = 0;
|
||||
int newlinePos = resultText.indexOf('\n', start);
|
||||
|
||||
while (newlinePos != -1 || start < static_cast<int>(resultText.length())) {
|
||||
String segment;
|
||||
if (newlinePos != -1) {
|
||||
segment = resultText.substring(start, newlinePos);
|
||||
start = newlinePos + 1;
|
||||
newlinePos = resultText.indexOf('\n', start);
|
||||
} else {
|
||||
segment = resultText.substring(start);
|
||||
start = resultText.length();
|
||||
}
|
||||
|
||||
if (display->getStringWidth(segment) <= maxWidth) {
|
||||
lines.push_back(segment);
|
||||
} else {
|
||||
// Try to break at better positions (space, >, <, -)
|
||||
String remaining = segment;
|
||||
|
||||
while (remaining.length() > 0) {
|
||||
String tempLine = "";
|
||||
int lastGoodBreak = -1;
|
||||
bool lineComplete = false;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(remaining.length()); i++) {
|
||||
char ch = remaining.charAt(i);
|
||||
String testLine = tempLine + ch;
|
||||
|
||||
if (display->getStringWidth(testLine) > maxWidth) {
|
||||
if (lastGoodBreak >= 0) {
|
||||
// Break at the last good position
|
||||
lines.push_back(remaining.substring(0, lastGoodBreak + 1));
|
||||
remaining = remaining.substring(lastGoodBreak + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else if (tempLine.length() > 0) {
|
||||
lines.push_back(tempLine);
|
||||
remaining = remaining.substring(i);
|
||||
lineComplete = true;
|
||||
break;
|
||||
} else {
|
||||
// Single character exceeds width
|
||||
lines.push_back(String(ch));
|
||||
remaining = remaining.substring(i + 1);
|
||||
lineComplete = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tempLine = testLine;
|
||||
// Mark good break positions
|
||||
if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') {
|
||||
lastGoodBreak = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineComplete) {
|
||||
// Reached end of remaining text
|
||||
if (tempLine.length() > 0) {
|
||||
lines.push_back(tempLine);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultLinesDirty) {
|
||||
rebuildResultLines(display);
|
||||
}
|
||||
|
||||
int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
for (size_t i = 0; i < resultLines.size(); i++) {
|
||||
int lineY = contentStartY + (i * lineHeight);
|
||||
if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) {
|
||||
display->drawString(x + 2, lineY, lines[i]);
|
||||
display->drawString(x + 2, lineY, resultLines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -715,7 +826,7 @@ int32_t TraceRouteModule::runOnce()
|
||||
if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) {
|
||||
LOG_INFO("TraceRoute timeout, no response received");
|
||||
runState = TRACEROUTE_STATE_RESULT;
|
||||
resultText = "No response received";
|
||||
setResultText("No response received");
|
||||
resultShowTime = now;
|
||||
tracingNode = 0;
|
||||
|
||||
@@ -751,6 +862,8 @@ int32_t TraceRouteModule::runOnce()
|
||||
// Cooldown finished
|
||||
LOG_INFO("TraceRoute cooldown finished, returning to IDLE");
|
||||
runState = TRACEROUTE_STATE_IDLE;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
bannerText = "";
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
@@ -764,6 +877,7 @@ int32_t TraceRouteModule::runOnce()
|
||||
LOG_INFO("TraceRoute result display timeout, returning to IDLE");
|
||||
runState = TRACEROUTE_STATE_IDLE;
|
||||
resultText = "";
|
||||
clearResultLines();
|
||||
bannerText = "";
|
||||
tracingNode = 0;
|
||||
UIFrameEvent e;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "OLEDDisplayUi.h"
|
||||
#endif
|
||||
#include <vector>
|
||||
|
||||
#define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0])
|
||||
|
||||
@@ -49,12 +50,23 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
void setResultText(const String &text);
|
||||
void clearResultLines();
|
||||
#if HAS_SCREEN
|
||||
void rebuildResultLines(OLEDDisplay *display);
|
||||
#endif
|
||||
// Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit
|
||||
void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination);
|
||||
|
||||
// Call to add your ID to the route array of a RouteDiscovery message
|
||||
void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly);
|
||||
|
||||
// Update next-hops in the routing table based on the returned route
|
||||
void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r);
|
||||
|
||||
// Helper to update next-hop for a single node
|
||||
void maybeSetNextHop(NodeNum target, uint8_t nextHopByte);
|
||||
|
||||
/* Call to print the route array of a RouteDiscovery message.
|
||||
Set origin to where the request came from.
|
||||
Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */
|
||||
@@ -68,6 +80,8 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
|
||||
unsigned long trackingTimeoutMs = 10000;
|
||||
String bannerText;
|
||||
String resultText;
|
||||
std::vector<String> resultLines;
|
||||
bool resultLinesDirty = false;
|
||||
NodeNum tracingNode = 0;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
@@ -116,6 +116,7 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
@@ -127,4 +128,4 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -157,6 +157,7 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
@@ -295,4 +296,4 @@ bool ICM20948Singleton::setWakeOnMotion()
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -59,7 +59,27 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length);
|
||||
return;
|
||||
}
|
||||
|
||||
const meshtastic_Channel &ch = channels.getByName(e.channel_id);
|
||||
// Find channel by channel_id and check downlink_enabled
|
||||
if (!(strcmp(e.channel_id, "PKI") == 0 ||
|
||||
(strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool anyChannelHasDownlink = false;
|
||||
size_t numChan = channels.getNumChannels();
|
||||
for (size_t i = 0; i < numChan; ++i) {
|
||||
const auto &c = channels.getByIndex(i);
|
||||
if (c.settings.downlink_enabled) {
|
||||
anyChannelHasDownlink = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(e.channel_id, "PKI") == 0 && !anyChannelHasDownlink) {
|
||||
return;
|
||||
}
|
||||
// Generate node ID from nodenum for comparison
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
if (strcmp(e.gateway_id, nodeId.c_str()) == 0) {
|
||||
@@ -77,11 +97,6 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
return;
|
||||
}
|
||||
|
||||
// Find channel by channel_id and check downlink_enabled
|
||||
if (!(strcmp(e.channel_id, "PKI") == 0 ||
|
||||
(strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) {
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Received MQTT topic %s, len=%u", topic, length);
|
||||
if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) {
|
||||
LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start);
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#include "BluetoothCommon.h"
|
||||
#include "NimbleBluetooth.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "StaticPointerQueue.h"
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "main.h"
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "sleep.h"
|
||||
#include <NimBLEDevice.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
@@ -32,45 +35,276 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// Debugging options: careful, they slow things down quite a bit!
|
||||
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
|
||||
// #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration
|
||||
// #define DEBUG_NIMBLE_NOTIFY // uncomment to enable notify logging
|
||||
|
||||
#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
|
||||
#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
|
||||
|
||||
NimBLECharacteristic *fromNumCharacteristic;
|
||||
NimBLECharacteristic *BatteryCharacteristic;
|
||||
NimBLECharacteristic *logRadioCharacteristic;
|
||||
NimBLEServer *bleServer;
|
||||
|
||||
static bool passkeyShowing;
|
||||
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
|
||||
class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
{
|
||||
/*
|
||||
CAUTION: There's a lot going on here and lots of room to break things.
|
||||
|
||||
This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and
|
||||
onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI).
|
||||
|
||||
The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to
|
||||
know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where
|
||||
locking isn't something that anyone has to worry about too much! :)
|
||||
|
||||
We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and
|
||||
handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about
|
||||
being run concurrently, which would make everything else much much much more complicated.
|
||||
|
||||
PHONE -> RADIO:
|
||||
- [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue.
|
||||
- [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls
|
||||
handleToRadio **in main task**.
|
||||
|
||||
RADIO -> PHONE:
|
||||
- [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless
|
||||
there's already a packet waiting in toPhoneQueue)
|
||||
- [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to
|
||||
get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the
|
||||
onReadCallbackIsWaitingForData flag.
|
||||
- [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex,
|
||||
pops the packet from toPhoneQueue, and returns it to NimBLE.
|
||||
|
||||
MUTEXES:
|
||||
- fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize
|
||||
- toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize
|
||||
|
||||
ATOMICS:
|
||||
- fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect).
|
||||
- toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect).
|
||||
- onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or
|
||||
onDisconnect).
|
||||
|
||||
PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio.
|
||||
|
||||
BLE CONNECTION PARAMS:
|
||||
- During config, we request a high-throughput, low-latency BLE connection for speed.
|
||||
- After config, we switch to a lower-power BLE connection for steady-state use to extend battery life.
|
||||
|
||||
MEMORY MANAGEMENT:
|
||||
- We keep packets on the stack and do not allocate heap.
|
||||
- We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks.
|
||||
- Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
|
||||
|
||||
NOTIFY IS BROKEN:
|
||||
- Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible.
|
||||
|
||||
ZERO-SIZE READS:
|
||||
- Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
|
||||
have data.
|
||||
- During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads
|
||||
until they get a 0-byte response.
|
||||
|
||||
CROSS-TASK WAKEUP:
|
||||
- If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data,
|
||||
- Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping.
|
||||
- Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause.
|
||||
*/
|
||||
|
||||
public:
|
||||
BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { nimble_queue.resize(3); }
|
||||
std::vector<NimBLEAttValue> nimble_queue;
|
||||
std::mutex nimble_mutex;
|
||||
uint8_t queue_size = 0;
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = 0;
|
||||
bool hasChecked = false;
|
||||
bool phoneWants = false;
|
||||
BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") {}
|
||||
|
||||
/* Packets from phone (BLE onWrite callback) */
|
||||
std::mutex fromPhoneMutex;
|
||||
std::atomic<size_t> fromPhoneQueueSize{0};
|
||||
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
|
||||
std::array<NimBLEAttValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
|
||||
|
||||
/* Packets to phone (BLE onRead callback) */
|
||||
std::mutex toPhoneMutex;
|
||||
std::atomic<size_t> toPhoneQueueSize{0};
|
||||
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
|
||||
std::array<std::array<uint8_t, meshtastic_FromRadio_size>, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{};
|
||||
std::array<size_t, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueueByteSizes{};
|
||||
// The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main
|
||||
// task's runOnce. It's only set by onRead, and only cleared by runOnce.
|
||||
std::atomic<bool> onReadCallbackIsWaitingForData{false};
|
||||
|
||||
/* Statistics/logging helpers */
|
||||
std::atomic<int32_t> readCount{0};
|
||||
std::atomic<int32_t> notifyCount{0};
|
||||
std::atomic<int32_t> writeCount{0};
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(nimble_mutex);
|
||||
if (queue_size > 0) {
|
||||
for (uint8_t i = 0; i < queue_size; i++) {
|
||||
handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length());
|
||||
}
|
||||
LOG_DEBUG("Queue_size %u", queue_size);
|
||||
queue_size = 0;
|
||||
}
|
||||
if (!hasChecked && phoneWants) {
|
||||
// Pull fresh data while we're outside of the NimBLE callback context.
|
||||
numBytes = getFromRadio(fromRadioBytes);
|
||||
hasChecked = true;
|
||||
while (runOnceHasWorkToDo()) {
|
||||
/*
|
||||
PROCESS fromPhoneQueue BEFORE toPhoneQueue:
|
||||
|
||||
In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same
|
||||
time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to
|
||||
service either the reads or writes first.
|
||||
|
||||
However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they
|
||||
expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to
|
||||
another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.)
|
||||
|
||||
So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous"
|
||||
write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is
|
||||
what the client wants!
|
||||
*/
|
||||
|
||||
// PHONE -> RADIO:
|
||||
runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
|
||||
|
||||
// RADIO -> PHONE:
|
||||
runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
|
||||
}
|
||||
|
||||
// the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
virtual void onConfigStart() override
|
||||
{
|
||||
LOG_INFO("BLE onConfigStart");
|
||||
|
||||
// Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
|
||||
if (bleServer && isConnected()) {
|
||||
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
requestHighThroughputConnection(conn_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void onConfigComplete() override
|
||||
{
|
||||
LOG_INFO("BLE onConfigComplete");
|
||||
|
||||
// Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
|
||||
if (bleServer && isConnected()) {
|
||||
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
requestLowerPowerConnection(conn_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); }
|
||||
|
||||
bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); }
|
||||
|
||||
bool runOnceToPhoneCanPreloadNextPacket()
|
||||
{
|
||||
/*
|
||||
* PRELOADING getFromRadio RESPONSES:
|
||||
*
|
||||
* It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call
|
||||
* getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet
|
||||
* forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where
|
||||
* the client might disconnect before completing the read.
|
||||
*
|
||||
* However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into
|
||||
* toPhoneQueue because the client will just reconnect after a disconnect, losing nothing.
|
||||
*/
|
||||
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
} else if (isSendingPackets()) {
|
||||
// If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio.
|
||||
return false;
|
||||
} else {
|
||||
// In other states, we can preload as long as there's space in the toPhoneQueue.
|
||||
return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
void runOnceHandleToPhoneQueue()
|
||||
{
|
||||
// Stack buffer for getFromRadio packet
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = 0;
|
||||
|
||||
if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) {
|
||||
numBytes = getFromRadio(fromRadioBytes);
|
||||
|
||||
if (numBytes == 0) {
|
||||
/*
|
||||
Client expected a read, but we have nothing to send.
|
||||
|
||||
In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
|
||||
notifies regularly, to make sure they have nothing else to read.
|
||||
|
||||
In other states, this is fine **so long as we've already processed pending onWrites first**, because the client
|
||||
may requesting wantConfig and immediately doing a read.
|
||||
*/
|
||||
} else {
|
||||
// Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
|
||||
if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
|
||||
// toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.)
|
||||
|
||||
{ // scope for toPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(toPhoneMutex);
|
||||
size_t storeAtIndex = toPhoneQueueSize.load();
|
||||
memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
|
||||
toPhoneQueueByteSizes[storeAtIndex] = numBytes;
|
||||
toPhoneQueueSize++;
|
||||
}
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes,
|
||||
toPhoneQueueSize.load());
|
||||
#endif
|
||||
} else {
|
||||
// Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full!
|
||||
LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed.
|
||||
onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push
|
||||
}
|
||||
}
|
||||
|
||||
bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; }
|
||||
|
||||
void runOnceHandleFromPhoneQueue()
|
||||
{
|
||||
// Handle packets we received from onWrite from the phone.
|
||||
if (fromPhoneQueueSize > 0) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *decreases*
|
||||
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.)
|
||||
|
||||
LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
|
||||
|
||||
// Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
|
||||
NimBLEAttValue val;
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(fromPhoneMutex);
|
||||
val = fromPhoneQueue[0];
|
||||
|
||||
// Shift the rest of the queue down
|
||||
for (uint8_t i = 1; i < fromPhoneQueueSize; i++) {
|
||||
fromPhoneQueue[i - 1] = fromPhoneQueue[i];
|
||||
}
|
||||
|
||||
// Safe decrement due to onDisconnect
|
||||
if (fromPhoneQueueSize > 0)
|
||||
fromPhoneQueueSize--;
|
||||
}
|
||||
|
||||
handleToRadio(val.data(), val.length());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
|
||||
*/
|
||||
@@ -78,14 +312,22 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
int currentNotifyCount = notifyCount.fetch_add(1);
|
||||
|
||||
uint8_t cc = bleServer->getConnectedCount();
|
||||
LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc);
|
||||
|
||||
#ifdef DEBUG_NIMBLE_NOTIFY
|
||||
// This logging slows things down when there are lots of packets going to the phone, like initial connection:
|
||||
LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
|
||||
#endif
|
||||
|
||||
uint8_t val[4];
|
||||
put_le32(val, fromRadioNum);
|
||||
|
||||
fromNumCharacteristic->setValue(val, sizeof(val));
|
||||
#ifdef NIMBLE_TWO
|
||||
// NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be
|
||||
// notify().
|
||||
fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
fromNumCharacteristic->notify();
|
||||
@@ -94,6 +336,54 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
|
||||
void requestHighThroughputConnection(uint16_t conn_handle)
|
||||
{
|
||||
/* Request a lower-latency, higher-throughput BLE connection.
|
||||
|
||||
This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to
|
||||
a slower mode.
|
||||
|
||||
See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
|
||||
constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
|
||||
recommendations.)
|
||||
|
||||
Selected settings:
|
||||
minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client
|
||||
supports it.)
|
||||
maxInterval (units of 1.25ms): 15ms = 12
|
||||
latency: 0 (don't allow peripheral to skip any connection events)
|
||||
timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
|
||||
|
||||
These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at
|
||||
setup. Not worth adjusting much.
|
||||
*/
|
||||
LOG_INFO("BLE requestHighThroughputConnection");
|
||||
bleServer->updateConnParams(conn_handle, 6, 12, 0, 600);
|
||||
}
|
||||
|
||||
void requestLowerPowerConnection(uint16_t conn_handle)
|
||||
{
|
||||
/* Request a lower power consumption (but higher latency, lower throughput) BLE connection.
|
||||
|
||||
This is suitable for steady-state operation after initial setup is complete.
|
||||
|
||||
See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
|
||||
constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
|
||||
recommendations.)
|
||||
|
||||
Selected settings:
|
||||
minInterval (units of 1.25ms): 30ms = 24
|
||||
maxInterval (units of 1.25ms): 50ms = 40
|
||||
latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power)
|
||||
timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
|
||||
|
||||
There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets
|
||||
per second.
|
||||
*/
|
||||
LOG_INFO("BLE requestLowerPowerConnection");
|
||||
bleServer->updateConnParams(conn_handle, 24, 40, 2, 600);
|
||||
}
|
||||
};
|
||||
|
||||
static BluetoothPhoneAPI *bluetoothPhoneAPI;
|
||||
@@ -113,18 +403,45 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
|
||||
#endif
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
// Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
|
||||
|
||||
int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1);
|
||||
|
||||
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
|
||||
int startMillis = millis();
|
||||
LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
|
||||
#endif
|
||||
|
||||
auto val = pCharacteristic->getValue();
|
||||
|
||||
if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
|
||||
if (bluetoothPhoneAPI->queue_size < 3) {
|
||||
if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
|
||||
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
|
||||
memcpy(lastToRadio, val.data(), val.length());
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex);
|
||||
bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val;
|
||||
bluetoothPhoneAPI->queue_size++;
|
||||
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
// Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize++;
|
||||
}
|
||||
|
||||
// After releasing the mutex, schedule immediate processing of the new packet.
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
|
||||
|
||||
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
|
||||
int finishMillis = millis();
|
||||
LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
|
||||
finishMillis - startMillis, val.length());
|
||||
#endif
|
||||
} else {
|
||||
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length());
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Drop duplicate ToRadio packet (%u bytes)", val.length());
|
||||
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -137,32 +454,107 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic)
|
||||
#endif
|
||||
{
|
||||
bluetoothPhoneAPI->phoneWants = true;
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex);
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
|
||||
if (!bluetoothPhoneAPI->hasChecked) {
|
||||
// Fetch payload on demand; prefetch keeps this fast for the first read.
|
||||
bluetoothPhoneAPI->numBytes = bluetoothPhoneAPI->getFromRadio(bluetoothPhoneAPI->fromRadioBytes);
|
||||
bluetoothPhoneAPI->hasChecked = true;
|
||||
}
|
||||
int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1);
|
||||
int tries = 0;
|
||||
int startMillis = millis();
|
||||
|
||||
pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes);
|
||||
|
||||
if (bluetoothPhoneAPI->numBytes != 0) {
|
||||
#ifdef NIMBLE_TWO
|
||||
// Notify immediately so subscribed clients see the packet without an extra read.
|
||||
pCharacteristic->notify(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes, BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
pCharacteristic->notify();
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis);
|
||||
#endif
|
||||
|
||||
// Is there a packet ready to go, or do we have to ask the main task to get one for us?
|
||||
if (bluetoothPhoneAPI->toPhoneQueueSize > 0) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *decreases*
|
||||
// toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.)
|
||||
|
||||
// There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData.
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount);
|
||||
#endif
|
||||
} else {
|
||||
// Tell the main task that we'd like a packet.
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
|
||||
|
||||
// Wait for the main task to produce a packet for us, up to about 20 seconds.
|
||||
// It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer
|
||||
// doing various setup tasks.
|
||||
while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) {
|
||||
// Schedule the main task runOnce to run ASAP.
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
|
||||
|
||||
if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) {
|
||||
// we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran
|
||||
// already
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount,
|
||||
millis() - startMillis, tries);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
// This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back.
|
||||
// No harm in polling pretty frequently.
|
||||
delay(tries < 20 ? 1 : 5);
|
||||
tries++;
|
||||
|
||||
if (tries == 4000) {
|
||||
LOG_WARN(
|
||||
"BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response",
|
||||
currentReadCount, millis() - startMillis, tries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload
|
||||
// Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet
|
||||
size_t numBytes = 0;
|
||||
{ // scope for toPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load();
|
||||
if (toPhoneQueueSize > 0) {
|
||||
// Copy from the front of the toPhoneQueue
|
||||
memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]);
|
||||
numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
|
||||
|
||||
// Shift the rest of the queue down
|
||||
for (uint8_t i = 1; i < toPhoneQueueSize; i++) {
|
||||
memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
|
||||
// The above line is similar to:
|
||||
// bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i]
|
||||
// but is usually faster because it doesn't have to copy all the trailing bytes beyond
|
||||
// toPhoneQueueByteSizes[i].
|
||||
//
|
||||
// We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic
|
||||
// memory allocations and frees across FreeRTOS tasks.
|
||||
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
|
||||
}
|
||||
|
||||
// Safe decrement due to onDisconnect
|
||||
if (bluetoothPhoneAPI->toPhoneQueueSize > 0)
|
||||
bluetoothPhoneAPI->toPhoneQueueSize--;
|
||||
} else {
|
||||
// nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0.
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
|
||||
int finishMillis = millis();
|
||||
LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount,
|
||||
finishMillis - startMillis, tries, numBytes);
|
||||
#endif
|
||||
|
||||
pCharacteristic->setValue(fromRadioBytes, numBytes);
|
||||
|
||||
// If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue.
|
||||
if (numBytes != 0) {
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
bluetoothPhoneAPI->numBytes = 0;
|
||||
bluetoothPhoneAPI->hasChecked = false;
|
||||
bluetoothPhoneAPI->phoneWants = false;
|
||||
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,6 +636,13 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
if (screen)
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// Store the connection handle for future use
|
||||
#ifdef NIMBLE_TWO
|
||||
nimbleBluetoothConnHandle = connInfo.getConnHandle();
|
||||
#else
|
||||
nimbleBluetoothConnHandle = desc->conn_handle;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
@@ -290,16 +689,29 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
if (bluetoothPhoneAPI) {
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex);
|
||||
bluetoothPhoneAPI->close();
|
||||
bluetoothPhoneAPI->numBytes = 0;
|
||||
bluetoothPhoneAPI->queue_size = 0;
|
||||
bluetoothPhoneAPI->hasChecked = false;
|
||||
bluetoothPhoneAPI->phoneWants = false;
|
||||
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize = 0;
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
|
||||
{ // scope for toPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
bluetoothPhoneAPI->toPhoneQueueSize = 0;
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->readCount = 0;
|
||||
bluetoothPhoneAPI->notifyCount = 0;
|
||||
bluetoothPhoneAPI->writeCount = 0;
|
||||
}
|
||||
|
||||
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
// Restart Advertising
|
||||
ble->startAdvertising();
|
||||
@@ -436,17 +848,15 @@ void NimbleBluetooth::setupService()
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE);
|
||||
// Allow notifications so phones can stream FromRadio without polling.
|
||||
FromRadioCharacteristic =
|
||||
bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ);
|
||||
fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
|
||||
logRadioCharacteristic =
|
||||
bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U);
|
||||
} else {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(
|
||||
TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC);
|
||||
FromRadioCharacteristic =
|
||||
bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN |
|
||||
NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::NOTIFY);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(
|
||||
FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
fromNumCharacteristic =
|
||||
bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ |
|
||||
NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
#define HW_VENDOR meshtastic_HardwareModel_CROWPANEL
|
||||
#elif defined(RAK3312)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_RAK3312
|
||||
#elif defined(RAK_WISMESH_TAP_V2)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP_V2
|
||||
#elif defined(LINK_32)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_LINK_32
|
||||
#elif defined(T_DECK_PRO)
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
// MAke sure all custom RAK4630 boards are defined before the generic RAK4630
|
||||
#elif defined(RAK4630)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_RAK4631
|
||||
#elif defined(RAK3401)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_RAK3401
|
||||
#elif defined(TTGO_T_ECHO)
|
||||
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO
|
||||
#elif defined(T_ECHO_LITE)
|
||||
|
||||
@@ -393,11 +393,17 @@ void portduinoSetup()
|
||||
// Need to bind all the configured GPIO pins so they're not simulated
|
||||
// TODO: If one of these fails, we should log and terminate
|
||||
for (auto i : portduino_config.all_pins) {
|
||||
if (i->enabled)
|
||||
// In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora
|
||||
// Those GPIO are handled in our usermode driver instead.
|
||||
if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") {
|
||||
continue;
|
||||
}
|
||||
if (i->enabled) {
|
||||
if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) {
|
||||
printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only initialize the radio pins when dealing with real, kernel controlled SPI hardware
|
||||
@@ -423,8 +429,7 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line)
|
||||
{
|
||||
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||
std::string gpio_name = "GPIO" + std::to_string(pinNum);
|
||||
std::cout << gpio_name;
|
||||
printf("\n");
|
||||
std::cout << "Initializing " << gpio_name << " on chip " << gpioChipName << std::endl;
|
||||
try {
|
||||
GPIOPin *csPin;
|
||||
csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str());
|
||||
|
||||
@@ -138,6 +138,7 @@ class Power : private concurrency::OSThread
|
||||
void reboot();
|
||||
// open circuit voltage lookup table
|
||||
uint8_t low_voltage_counter;
|
||||
int32_t lastLogTime = 0;
|
||||
#ifdef DEBUG_HEAP
|
||||
uint32_t lastheap;
|
||||
#endif
|
||||
|
||||
@@ -244,6 +244,10 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
// pinMode(PIN_POWER_EN1, INPUT_PULLDOWN);
|
||||
#endif
|
||||
|
||||
#ifdef RAK_WISMESH_TAP_V2
|
||||
digitalWrite(SDCARD_CS, LOW);
|
||||
#endif
|
||||
|
||||
#ifdef TRACKER_T1000_E
|
||||
#ifdef GNSS_AIROHA
|
||||
digitalWrite(GPS_VRTC_EN, LOW);
|
||||
|
||||
@@ -5,3 +5,12 @@ board_check = true
|
||||
build_flags =
|
||||
${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16
|
||||
upload_speed = 115200
|
||||
|
||||
[env:sugarcube]
|
||||
extends = env:tlora-v2-1-1_6
|
||||
board_level = extra
|
||||
build_flags =
|
||||
${env:tlora-v2-1-1_6.build_flags}
|
||||
-DBUTTON_PIN=0
|
||||
-DPIN_BUZZER=25
|
||||
-DLED_PIN=-1
|
||||
@@ -8,7 +8,11 @@
|
||||
#define I2C_SDA 21 // I2C pins for this board
|
||||
#define I2C_SCL 22
|
||||
|
||||
#if defined(LED_PIN) && LED_PIN == -1
|
||||
#undef LED_PIN
|
||||
#else
|
||||
#define LED_PIN 25 // If defined we will blink this LED
|
||||
#endif
|
||||
|
||||
#define USE_RF95
|
||||
#define LORA_DIO0 26 // a No connect on the SX1262 module
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#define BATTERY_PIN 8
|
||||
#define ADC_CHANNEL ADC1_GPIO8_CHANNEL
|
||||
|
||||
#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage.
|
||||
|
||||
#define PIN_BUZZER 9
|
||||
|
||||
// Buttons
|
||||
|
||||
@@ -13,8 +13,8 @@ static const uint8_t LED_BUILTIN = 35;
|
||||
static const uint8_t TX = 43;
|
||||
static const uint8_t RX = 44;
|
||||
|
||||
static const uint8_t SDA = 3;
|
||||
static const uint8_t SCL = 4;
|
||||
static const uint8_t SDA = 4;
|
||||
static const uint8_t SCL = 3;
|
||||
|
||||
static const uint8_t SS = 8;
|
||||
static const uint8_t MOSI = 10;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[env:heltec-v4]
|
||||
[heltec_v4_base]
|
||||
extends = esp32s3_base
|
||||
board = heltec_v4
|
||||
board_check = true
|
||||
@@ -7,3 +7,106 @@ build_flags =
|
||||
${esp32s3_base.build_flags}
|
||||
-D HELTEC_V4
|
||||
-I variants/esp32s3/heltec_v4
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
|
||||
|
||||
[env:heltec-v4]
|
||||
extends = heltec_v4_base
|
||||
build_flags =
|
||||
${heltec_v4_base.build_flags}
|
||||
-D HELTEC_V4_OLED
|
||||
-D USE_SSD1306 ; Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver)
|
||||
-D LED_PIN=35
|
||||
-D RESET_OLED=21
|
||||
-D I2C_SDA=17
|
||||
-D I2C_SCL=18
|
||||
-D I2C_SDA1=4
|
||||
-D I2C_SCL1=3
|
||||
lib_deps =
|
||||
${heltec_v4_base.lib_deps}
|
||||
|
||||
[env:heltec-v4-tft]
|
||||
extends = heltec_v4_base
|
||||
build_flags =
|
||||
${heltec_v4_base.build_flags} ;-Os
|
||||
-D HELTEC_V4_TFT
|
||||
-D I2C_SDA=4
|
||||
-D I2C_SCL=3
|
||||
-D I2C_SDA1=47
|
||||
-D I2C_SCL1=48
|
||||
-D PIN_BUTTON2=35
|
||||
-D PIN_BUZZER=6
|
||||
-D USE_PIN_BUZZER=PIN_BUZZER
|
||||
-D CONFIG_ARDUHAL_LOG_COLORS
|
||||
-D RADIOLIB_DEBUG_SPI=0
|
||||
-D RADIOLIB_DEBUG_PROTOCOL=0
|
||||
-D RADIOLIB_DEBUG_BASIC=0
|
||||
-D RADIOLIB_VERBOSE_ASSERT=0
|
||||
-D RADIOLIB_SPI_PARANOID=0
|
||||
-D CONFIG_DISABLE_HAL_LOCKS=1
|
||||
-D INPUTDRIVER_BUTTON_TYPE=0
|
||||
-D HAS_SCREEN=1
|
||||
-D HAS_TFT=1
|
||||
-D RAM_SIZE=1560
|
||||
-D LV_LVGL_H_INCLUDE_SIMPLE
|
||||
-D LV_CONF_INCLUDE_SIMPLE
|
||||
-D LV_COMP_CONF_INCLUDE_SIMPLE
|
||||
-D LV_USE_SYSMON=0
|
||||
-D LV_USE_PROFILER=0
|
||||
-D LV_USE_PERF_MONITOR=0
|
||||
-D LV_USE_MEM_MONITOR=0
|
||||
-D LV_USE_LOG=0
|
||||
-D LV_BUILD_TEST=0
|
||||
-D USE_LOG_DEBUG
|
||||
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
|
||||
-D USE_PACKET_API
|
||||
-D LGFX_DRIVER=LGFX_HELTEC_V4_TFT
|
||||
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\"
|
||||
-D VIEW_320x240
|
||||
-D MAP_FULL_REDRAW
|
||||
-D DISPLAY_SIZE=320x240 ; landscape mode
|
||||
-D LGFX_PIN_SCK=17
|
||||
-D LGFX_PIN_MOSI=33
|
||||
-D LGFX_PIN_DC=16
|
||||
-D LGFX_PIN_CS=15
|
||||
-D LGFX_PIN_BL=21
|
||||
-D LGFX_PIN_RST=18
|
||||
-D CUSTOM_TOUCH_DRIVER
|
||||
-D TOUCH_SDA_PIN=I2C_SDA1
|
||||
-D TOUCH_SCL_PIN=I2C_SCL1
|
||||
-D TOUCH_INT_PIN=-1 ;45
|
||||
-D TOUCH_RST_PIN=44
|
||||
;base UI
|
||||
-D TFT_CS=LGFX_PIN_CS
|
||||
-D ST7789_CS=TFT_CS
|
||||
-D ST7789_RS=LGFX_PIN_DC
|
||||
-D ST7789_SDA=LGFX_PIN_MOSI
|
||||
-D ST7789_SCK=LGFX_PIN_SCK
|
||||
-D ST7789_RESET=LGFX_PIN_RST
|
||||
-D ST7789_MISO=-1
|
||||
-D ST7789_BUSY=-1
|
||||
-D ST7789_BL=LGFX_PIN_BL
|
||||
-D ST7789_SPI_HOST=SPI3_HOST
|
||||
-D TFT_BL=ST7789_BL
|
||||
-D SPI_FREQUENCY=40000000
|
||||
-D SPI_READ_FREQUENCY=4000000
|
||||
-D TFT_HEIGHT=320
|
||||
-D TFT_WIDTH=240
|
||||
-D TFT_OFFSET_X=0
|
||||
-D TFT_OFFSET_Y=0
|
||||
-D TFT_OFFSET_ROTATION=0
|
||||
-D SCREEN_ROTATE
|
||||
-D SCREEN_TRANSITION_FRAMERATE=5
|
||||
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
|
||||
-D HAS_TOUCHSCREEN=1
|
||||
-D TOUCH_I2C_PORT=0
|
||||
-D TOUCH_SLAVE_ADDRESS=0x2E
|
||||
-D SCREEN_TOUCH_INT=TOUCH_INT_PIN
|
||||
-D SCREEN_TOUCH_RST=TOUCH_RST_PIN
|
||||
|
||||
lib_deps = ${heltec_v4_base.lib_deps}
|
||||
; ${device-ui_base.lib_deps}
|
||||
lovyan03/LovyanGFX@1.2.0
|
||||
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
||||
https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
#define LED_PIN 35
|
||||
|
||||
#define USE_SSD1306 // Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver)
|
||||
|
||||
#define RESET_OLED 21
|
||||
#define I2C_SDA 17 // I2C pins for this board
|
||||
#define I2C_SCL 18
|
||||
|
||||
#define VEXT_ENABLE 36 // active low, powers the oled display and the lora antenna boost
|
||||
#define BUTTON_PIN 0
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ extends = native_base
|
||||
build_flags = ${native_base.build_flags}
|
||||
!pkg-config --libs libulfius --silence-errors || :
|
||||
!pkg-config --libs openssl --silence-errors || :
|
||||
!pkg-config --cflags --libs sdl2 --silence-errors || :
|
||||
!pkg-config --cflags --libs libbsd-overlay --silence-errors || :
|
||||
|
||||
[env:native-tft]
|
||||
extends = native_base
|
||||
@@ -42,6 +44,7 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio
|
||||
!pkg-config --libs libulfius --silence-errors || :
|
||||
!pkg-config --libs openssl --silence-errors || :
|
||||
!pkg-config --cflags --libs sdl2 --silence-errors || :
|
||||
!pkg-config --cflags --libs libbsd-overlay --silence-errors || :
|
||||
build_src_filter =
|
||||
${native_base.build_src_filter}
|
||||
|
||||
@@ -70,6 +73,7 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections
|
||||
-D MAP_FULL_REDRAW
|
||||
!pkg-config --libs libulfius --silence-errors || :
|
||||
!pkg-config --libs openssl --silence-errors || :
|
||||
!pkg-config --cflags --libs libbsd-overlay --silence-errors || :
|
||||
build_src_filter =
|
||||
${native_base.build_src_filter}
|
||||
|
||||
@@ -102,6 +106,7 @@ build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -l
|
||||
-D VIEW_320x240
|
||||
!pkg-config --libs libulfius --silence-errors || :
|
||||
!pkg-config --libs openssl --silence-errors || :
|
||||
!pkg-config --cflags --libs libbsd-overlay --silence-errors || :
|
||||
build_src_filter = ${env:native-tft.build_src_filter}
|
||||
|
||||
[env:coverage]
|
||||
|
||||
31
variants/nrf52840/rak3401_1watt/platformio.ini
Normal file
31
variants/nrf52840/rak3401_1watt/platformio.ini
Normal file
@@ -0,0 +1,31 @@
|
||||
; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921
|
||||
[env:rak3401-1watt]
|
||||
extends = nrf52840_base
|
||||
board = wiscore_rak4631
|
||||
board_check = true
|
||||
build_flags = ${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/rak3401_1watt
|
||||
-D RAK_4631
|
||||
; -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely.
|
||||
-D RAK3401
|
||||
-D RAK13302 ; RAK 1Watt Power Amplifier
|
||||
-DRADIOLIB_EXCLUDE_SX128X=1
|
||||
-DRADIOLIB_EXCLUDE_SX127X=1
|
||||
-DRADIOLIB_EXCLUDE_LR11X0=1
|
||||
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> +<mesh/api/>
|
||||
lib_deps =
|
||||
${nrf52840_base.lib_deps}
|
||||
${networking_base.lib_deps}
|
||||
melopero/Melopero RV3028@^1.1.0
|
||||
rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2
|
||||
beegee-tokyo/RAK12035_SoilMoisture@^1.0.4
|
||||
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
|
||||
|
||||
; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm)
|
||||
; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds
|
||||
;upload_protocol = jlink
|
||||
|
||||
; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!)
|
||||
; programming time is about the same as the bootloader version.
|
||||
; For information on this see the meshtastic developers documentation for "Development on the NRF52"
|
||||
|
||||
45
variants/nrf52840/rak3401_1watt/variant.cpp
Normal file
45
variants/nrf52840/rak3401_1watt/variant.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "variant.h"
|
||||
#include "nrf.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
// P0
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||
|
||||
// P1
|
||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
|
||||
void initVariant()
|
||||
{
|
||||
// LED1 & LED2
|
||||
pinMode(PIN_LED1, OUTPUT);
|
||||
ledOff(PIN_LED1);
|
||||
|
||||
pinMode(PIN_LED2, OUTPUT);
|
||||
ledOff(PIN_LED2);
|
||||
|
||||
// 3V3 Power Rail
|
||||
pinMode(PIN_3V3_EN, OUTPUT);
|
||||
digitalWrite(PIN_3V3_EN, HIGH);
|
||||
}
|
||||
226
variants/nrf52840/rak3401_1watt/variant.h
Normal file
226
variants/nrf52840/rak3401_1watt/variant.h
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
|
||||
Copyright (c) 2016 Sandeep Mistry All right reserved.
|
||||
Copyright (c) 2018, Adafruit Industries (adafruit.com)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _VARIANT_RAK3401_
|
||||
#define _VARIANT_RAK3401_
|
||||
|
||||
#define RAK4630
|
||||
|
||||
/** Master clock frequency */
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
#define USE_LFXO // Board uses 32khz crystal for LF
|
||||
// define USE_LFRC // Board uses RC for LF
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Headers
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
// Number of pins defined in PinDescription array
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (6)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
// LEDs
|
||||
#define PIN_LED1 (35)
|
||||
#define PIN_LED2 (36)
|
||||
|
||||
#define LED_BUILTIN PIN_LED1
|
||||
#define LED_CONN PIN_LED2
|
||||
|
||||
#define LED_GREEN PIN_LED1
|
||||
#define LED_BLUE PIN_LED2
|
||||
|
||||
#define LED_STATE_ON 1 // State when LED is litted
|
||||
|
||||
/*
|
||||
* Analog pins
|
||||
*/
|
||||
#define PIN_A0 (5)
|
||||
#define PIN_A1 (31)
|
||||
#define PIN_A2 (28)
|
||||
#define PIN_A3 (29)
|
||||
#define PIN_A4 (30)
|
||||
#define PIN_A5 (31)
|
||||
#define PIN_A6 (0xff)
|
||||
#define PIN_A7 (0xff)
|
||||
|
||||
static const uint8_t A0 = PIN_A0;
|
||||
static const uint8_t A1 = PIN_A1;
|
||||
static const uint8_t A2 = PIN_A2;
|
||||
static const uint8_t A3 = PIN_A3;
|
||||
static const uint8_t A4 = PIN_A4;
|
||||
static const uint8_t A5 = PIN_A5;
|
||||
static const uint8_t A6 = PIN_A6;
|
||||
static const uint8_t A7 = PIN_A7;
|
||||
#define ADC_RESOLUTION 14
|
||||
|
||||
// Other pins
|
||||
#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
|
||||
#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
|
||||
|
||||
#define PIN_AREF (2)
|
||||
#define PIN_NFC1 (9)
|
||||
#define WB_IO5 PIN_NFC1
|
||||
#define WB_IO4 (4)
|
||||
#define PIN_NFC2 (10)
|
||||
|
||||
static const uint8_t AREF = PIN_AREF;
|
||||
|
||||
/*
|
||||
* Serial interfaces
|
||||
*/
|
||||
#define PIN_SERIAL1_RX (15)
|
||||
#define PIN_SERIAL1_TX (16)
|
||||
|
||||
// Connected to Jlink CDC
|
||||
#define PIN_SERIAL2_RX (8)
|
||||
#define PIN_SERIAL2_TX (6)
|
||||
|
||||
/*
|
||||
* SPI Interfaces
|
||||
*/
|
||||
#define SPI_INTERFACES_COUNT 2
|
||||
|
||||
#define PIN_SPI_MISO (45)
|
||||
#define PIN_SPI_MOSI (44)
|
||||
#define PIN_SPI_SCK (43)
|
||||
|
||||
#define PIN_SPI1_MISO (29) // (0 + 29)
|
||||
#define PIN_SPI1_MOSI (30) // (0 + 30)
|
||||
#define PIN_SPI1_SCK (3) // (0 + 3)
|
||||
|
||||
static const uint8_t SS = 42;
|
||||
static const uint8_t MOSI = PIN_SPI_MOSI;
|
||||
static const uint8_t MISO = PIN_SPI_MISO;
|
||||
static const uint8_t SCK = PIN_SPI_SCK;
|
||||
|
||||
/*
|
||||
* eink display pins
|
||||
*/
|
||||
#define PIN_EINK_CS (0 + 26)
|
||||
#define PIN_EINK_BUSY (0 + 4)
|
||||
#define PIN_EINK_DC (0 + 17)
|
||||
#define PIN_EINK_RES (-1)
|
||||
#define PIN_EINK_SCLK (0 + 3)
|
||||
#define PIN_EINK_MOSI (0 + 30) // also called SDI
|
||||
|
||||
/*
|
||||
* Wire Interfaces
|
||||
*/
|
||||
#define WIRE_INTERFACES_COUNT 1
|
||||
|
||||
#define PIN_WIRE_SDA (WB_I2C1_SDA)
|
||||
#define PIN_WIRE_SCL (WB_I2C1_SCL)
|
||||
|
||||
// QSPI Pins
|
||||
#define PIN_QSPI_SCK 3
|
||||
#define PIN_QSPI_CS 26
|
||||
#define PIN_QSPI_IO0 30
|
||||
#define PIN_QSPI_IO1 29
|
||||
#define PIN_QSPI_IO2 28
|
||||
#define PIN_QSPI_IO3 2
|
||||
|
||||
// On-board QSPI Flash
|
||||
#define EXTERNAL_FLASH_DEVICES IS25LP080D
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
|
||||
// 1watt sx1262 RAK13302
|
||||
#define HW_SPI1_DEVICE 1
|
||||
|
||||
#define LORA_SCK PIN_SPI1_SCK
|
||||
#define LORA_MISO PIN_SPI1_MISO
|
||||
#define LORA_MOSI PIN_SPI1_MOSI
|
||||
#define LORA_CS 26
|
||||
|
||||
#define USE_SX1262
|
||||
#define SX126X_CS (26)
|
||||
#define SX126X_DIO1 (10)
|
||||
#define SX126X_BUSY (9)
|
||||
#define SX126X_RESET (4)
|
||||
|
||||
#define SX126X_POWER_EN (21)
|
||||
// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
// Testing USB detection
|
||||
#define NRF_APM
|
||||
// If using a power chip like the INA3221 you can override the default battery voltage channel below
|
||||
// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging
|
||||
// #define INA3221_BAT_CH INA3221_CH2
|
||||
// #define INA3221_ENV_CH INA3221_CH1
|
||||
|
||||
// enables 3.3V periphery like GPS or IO Module
|
||||
// Do not toggle this for GPS power savings
|
||||
#define PIN_3V3_EN (34)
|
||||
#define WB_IO2 PIN_3V3_EN
|
||||
|
||||
// RAK1910 GPS module
|
||||
// If using the wisblock GPS module and pluged into Port A on WisBlock base
|
||||
// IO1 is hooked to PPS (pin 12 on header) = gpio 17
|
||||
// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on).
|
||||
// Therefore must be 1 to keep peripherals powered
|
||||
// Power is on the controllable 3V3_S rail
|
||||
// #define PIN_GPS_RESET (34)
|
||||
// #define PIN_GPS_EN PIN_3V3_EN
|
||||
#define PIN_GPS_PPS (17) // Pulse per second input from the GPS
|
||||
|
||||
#define GPS_RX_PIN PIN_SERIAL1_RX
|
||||
#define GPS_TX_PIN PIN_SERIAL1_TX
|
||||
|
||||
// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press
|
||||
|
||||
// RAK12002 RTC Module
|
||||
#define RV3028_RTC (uint8_t)0b1010010
|
||||
|
||||
// RAK18001 Buzzer in Slot C
|
||||
// #define PIN_BUZZER 21 // IO3 is PWM2
|
||||
// NEW: set this via protobuf instead!
|
||||
|
||||
// Battery
|
||||
// The battery sense is hooked to pin A0 (5)
|
||||
#define BATTERY_PIN PIN_A0
|
||||
// and has 12 bit resolution
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#define BATTERY_SENSE_RESOLUTION 4096.0
|
||||
#undef AREF_VOLTAGE
|
||||
#define AREF_VOLTAGE 3.0
|
||||
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
|
||||
#define ADC_MULTIPLIER 1.73
|
||||
|
||||
#define HAS_RTC 1
|
||||
|
||||
#define RAK_4631 1
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/*----------------------------------------------------------------------------
|
||||
* Arduino objects - C++ only
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
[VERSION]
|
||||
major = 2
|
||||
minor = 7
|
||||
build = 13
|
||||
build = 14
|
||||
|
||||
Reference in New Issue
Block a user