mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 14:52:32 +00:00
Compare commits
286 Commits
split-noti
...
on-screen-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c95991cee2 | ||
|
|
f901166f16 | ||
|
|
ddc66c5e84 | ||
|
|
0544998c9b | ||
|
|
093a37a2b0 | ||
|
|
1daf5aad1f | ||
|
|
fe3f14a63e | ||
|
|
dca615fe3d | ||
|
|
cca2ead2c1 | ||
|
|
7574bfb7cb | ||
|
|
ce75bf4496 | ||
|
|
5ce47045e7 | ||
|
|
57e1725419 | ||
|
|
53afebae41 | ||
|
|
11309662a9 | ||
|
|
890357d579 | ||
|
|
4f4ede9c8c | ||
|
|
f413c49555 | ||
|
|
c19f573b49 | ||
|
|
5de61b1a3d | ||
|
|
1c1462e776 | ||
|
|
eb6ef1cbea | ||
|
|
9654f5b218 | ||
|
|
68726a1b0e | ||
|
|
5b62bbe8e6 | ||
|
|
e55084629a | ||
|
|
1691e885f2 | ||
|
|
2d7818797d | ||
|
|
f65e2c639e | ||
|
|
fbd4138d98 | ||
|
|
f4bb2ec0f0 | ||
|
|
95200e8f6b | ||
|
|
36e8dc74f4 | ||
|
|
995752e31d | ||
|
|
75b12d318d | ||
|
|
eea4d734d2 | ||
|
|
78c5309e9a | ||
|
|
9feb1d378e | ||
|
|
e5e8683cdb | ||
|
|
d538ad170c | ||
|
|
c64c196778 | ||
|
|
8e552a9f0c | ||
|
|
e98c6debb2 | ||
|
|
a02017a5c8 | ||
|
|
0046d957f1 | ||
|
|
4a241deb96 | ||
|
|
8d5ae1d5d2 | ||
|
|
e1e89a5e62 | ||
|
|
a7be93449e | ||
|
|
c8694f9f2d | ||
|
|
ace45c1236 | ||
|
|
062168cd42 | ||
|
|
1877a2c531 | ||
|
|
52f0e5a3db | ||
|
|
6c7da1e6b1 | ||
|
|
ac8c372349 | ||
|
|
1bfa429c38 | ||
|
|
ddd149945a | ||
|
|
e3dd8164a4 | ||
|
|
9b8149f14e | ||
|
|
05f1518951 | ||
|
|
e26de85b5f | ||
|
|
a2df80e833 | ||
|
|
db238ef524 | ||
|
|
f2b935f48f | ||
|
|
e69da71d4e | ||
|
|
7505fe7a7c | ||
|
|
f6857f1bcb | ||
|
|
7fe2c74139 | ||
|
|
be60f9612e | ||
|
|
2de9f015b1 | ||
|
|
c1f4f79d4a | ||
|
|
7b874cf597 | ||
|
|
8568b56ac6 | ||
|
|
f2a880f813 | ||
|
|
691327b2db | ||
|
|
a23c58c10a | ||
|
|
27c6b24e3a | ||
|
|
384436e937 | ||
|
|
eb30aae486 | ||
|
|
079286da04 | ||
|
|
0130899b3b | ||
|
|
d1f3c3c982 | ||
|
|
3b6eefa8bb | ||
|
|
5107531425 | ||
|
|
88655ffc44 | ||
|
|
10bd10b9d1 | ||
|
|
956a0f102b | ||
|
|
bdedd0e1fe | ||
|
|
4c901033b2 | ||
|
|
7d926da98c | ||
|
|
1b793d1f23 | ||
|
|
b5a8e8f51b | ||
|
|
cc5d00e211 | ||
|
|
1a8ab2aadc | ||
|
|
608fdc6f52 | ||
|
|
1d8638b47d | ||
|
|
3ecff48722 | ||
|
|
aa3b14ce72 | ||
|
|
28aeb0f09e | ||
|
|
7c5e2c5393 | ||
|
|
df8b629c2c | ||
|
|
a506dc6b65 | ||
|
|
fc1e6ccb8c | ||
|
|
bbc638ab82 | ||
|
|
4f57a2e248 | ||
|
|
4c6db2c5bd | ||
|
|
bbe548bc98 | ||
|
|
d1fbf65c5d | ||
|
|
7a4a915312 | ||
|
|
4f895f744b | ||
|
|
66a831dfa8 | ||
|
|
516597a73e | ||
|
|
4eb6c9fb8e | ||
|
|
46e2ae8860 | ||
|
|
54c0cbeb66 | ||
|
|
82ddf4732a | ||
|
|
ed0cdefb44 | ||
|
|
8836be0f47 | ||
|
|
96f63f3945 | ||
|
|
d80dcd6afd | ||
|
|
2087629a47 | ||
|
|
878d68c5ef | ||
|
|
86960cdb1d | ||
|
|
fff12979a2 | ||
|
|
6c12baf4ed | ||
|
|
29449a71d4 | ||
|
|
9b983b6487 | ||
|
|
806bfa54b5 | ||
|
|
920aeeeba5 | ||
|
|
32418448de | ||
|
|
b3525c2569 | ||
|
|
19dc2873c5 | ||
|
|
25b8d9b0ca | ||
|
|
8aef3c44f4 | ||
|
|
8345c21eff | ||
|
|
36b94cf823 | ||
|
|
475cfe4af2 | ||
|
|
b851b15a73 | ||
|
|
73347c2542 | ||
|
|
bc9023399d | ||
|
|
a9c9b96eb6 | ||
|
|
1c2a3c620f | ||
|
|
9313d04726 | ||
|
|
44518fea14 | ||
|
|
91049d0db3 | ||
|
|
855514b4f3 | ||
|
|
974741a366 | ||
|
|
5d98f7e307 | ||
|
|
3ca45ae99c | ||
|
|
cf574c71d8 | ||
|
|
abe0a34fc0 | ||
|
|
71b6508ad3 | ||
|
|
55fc4fcd90 | ||
|
|
c3b2b474c6 | ||
|
|
39716ed1ba | ||
|
|
625a529f6c | ||
|
|
31d56c16d5 | ||
|
|
5776385e8c | ||
|
|
8f10de5684 | ||
|
|
e864fcf9a8 | ||
|
|
86af5f5252 | ||
|
|
daa1d582cb | ||
|
|
f197f0e5ec | ||
|
|
3599ca6845 | ||
|
|
1be4fc5ae9 | ||
|
|
ac3e5684d6 | ||
|
|
29cca4d621 | ||
|
|
f3ff80963a | ||
|
|
45e428eb25 | ||
|
|
16d2650236 | ||
|
|
2ecbf704d0 | ||
|
|
5e28ee6d1e | ||
|
|
622023de8b | ||
|
|
b49e59b904 | ||
|
|
0133c5dc9e | ||
|
|
fd414ed149 | ||
|
|
77768e9023 | ||
|
|
86be2ac12f | ||
|
|
4342d51f5a | ||
|
|
41f52a6566 | ||
|
|
cb47325f08 | ||
|
|
deed6cd96a | ||
|
|
05c32c99e4 | ||
|
|
1ca0584ba0 | ||
|
|
5ae8021aa6 | ||
|
|
9798a91e7b | ||
|
|
e9a551ae90 | ||
|
|
d42bde135f | ||
|
|
72f3d19d5a | ||
|
|
f7ecf141b5 | ||
|
|
1063ef9034 | ||
|
|
13ac182142 | ||
|
|
4bab148e3b | ||
|
|
be75f11156 | ||
|
|
093868f3ed | ||
|
|
fe534eae37 | ||
|
|
1aad442ccc | ||
|
|
57c1c9286b | ||
|
|
6030bf50e0 | ||
|
|
6d8c815558 | ||
|
|
5f5698ccc0 | ||
|
|
74c735d5fb | ||
|
|
107dec22bd | ||
|
|
0795b21c2b | ||
|
|
a7e516d6f6 | ||
|
|
f6d378255c | ||
|
|
19d831d20d | ||
|
|
00495140bd | ||
|
|
354f149338 | ||
|
|
999e1207a5 | ||
|
|
916587c2a6 | ||
|
|
db4e4e6e53 | ||
|
|
9c08220d24 | ||
|
|
88b299dd41 | ||
|
|
19af2d9e3b | ||
|
|
fa23be4424 | ||
|
|
e1f40c2db9 | ||
|
|
415dc4aa47 | ||
|
|
f2fb473ecf | ||
|
|
f95c77b8bd | ||
|
|
09d4ee1ea7 | ||
|
|
40c586ca97 | ||
|
|
708978911b | ||
|
|
2a26209889 | ||
|
|
1378657206 | ||
|
|
98d010761e | ||
|
|
798b1f4d86 | ||
|
|
29893e0c28 | ||
|
|
1994bb3cd1 | ||
|
|
f35ca812a3 | ||
|
|
abbeb4874d | ||
|
|
0f96bd7a26 | ||
|
|
dfb07e8bd2 | ||
|
|
ff4eed08bc | ||
|
|
f13dc5b903 | ||
|
|
c1431f4f9a | ||
|
|
93132fad28 | ||
|
|
2254d551f4 | ||
|
|
f2d3f54824 | ||
|
|
6fa597bc5d | ||
|
|
b02e58521d | ||
|
|
81828c6244 | ||
|
|
549250b91a | ||
|
|
409dfe22ae | ||
|
|
a6be2e46ed | ||
|
|
3fdefe8289 | ||
|
|
1f85e2a02a | ||
|
|
f99ac2104c | ||
|
|
553fc0cb1b | ||
|
|
f39c7ad47e | ||
|
|
e505ec847e | ||
|
|
90e99b2bac | ||
|
|
d25240b33b | ||
|
|
d494c23a88 | ||
|
|
17f8303e01 | ||
|
|
aadea89202 | ||
|
|
53013e9a7e | ||
|
|
30eec01f55 | ||
|
|
cc961d7762 | ||
|
|
a7528d777a | ||
|
|
13013a272f | ||
|
|
3ea96bb6e1 | ||
|
|
baf0e9c7e6 | ||
|
|
598eebfb10 | ||
|
|
5841c889ba | ||
|
|
4bd416413a | ||
|
|
be06a7d881 | ||
|
|
26df4f8142 | ||
|
|
b6a13f1114 | ||
|
|
2bcf608654 | ||
|
|
705515ace2 | ||
|
|
a97df4bb52 | ||
|
|
f6743798e2 | ||
|
|
2ea70927c8 | ||
|
|
de5b55921e | ||
|
|
2b97576b18 | ||
|
|
29e7a71c97 | ||
|
|
18fbc2149d | ||
|
|
50424d1035 | ||
|
|
e87c991975 | ||
|
|
2ab717cebb | ||
|
|
ad23c065f6 | ||
|
|
eeb52a1221 | ||
|
|
8ae05f6b33 | ||
|
|
f6630cd31d |
50
.github/actions/build-variant/action.yml
vendored
50
.github/actions/build-variant/action.yml
vendored
@@ -27,10 +27,10 @@ inputs:
|
|||||||
description: A newline separated list of paths to store as artifacts
|
description: A newline separated list of paths to store as artifacts
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
include-web-ui:
|
# include-web-ui:
|
||||||
description: Include the web UI in the build
|
# description: Include the web UI in the build
|
||||||
required: false
|
# required: false
|
||||||
default: "false"
|
# default: "false"
|
||||||
arch:
|
arch:
|
||||||
description: Processor arch name
|
description: Processor arch name
|
||||||
required: true
|
required: true
|
||||||
@@ -43,29 +43,29 @@ runs:
|
|||||||
id: base
|
id: base
|
||||||
uses: ./.github/actions/setup-base
|
uses: ./.github/actions/setup-base
|
||||||
|
|
||||||
- name: Get web ui version
|
# - name: Get web ui version
|
||||||
if: inputs.include-web-ui == 'true'
|
# if: inputs.include-web-ui == 'true'
|
||||||
id: webver
|
# id: webver
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT
|
# echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Pull web ui
|
# - name: Pull web ui
|
||||||
if: inputs.include-web-ui == 'true'
|
# if: inputs.include-web-ui == 'true'
|
||||||
uses: dsaltares/fetch-gh-release-asset@master
|
# uses: dsaltares/fetch-gh-release-asset@master
|
||||||
with:
|
# with:
|
||||||
repo: meshtastic/web
|
# repo: meshtastic/web
|
||||||
file: build.tar
|
# file: build.tar
|
||||||
target: build.tar
|
# target: build.tar
|
||||||
token: ${{ inputs.github_token }}
|
# token: ${{ inputs.github_token }}
|
||||||
version: tags/v${{ steps.webver.outputs.ver }}
|
# version: tags/v${{ steps.webver.outputs.ver }}
|
||||||
|
|
||||||
- name: Unpack web ui
|
# - name: Unpack web ui
|
||||||
if: inputs.include-web-ui == 'true'
|
# if: inputs.include-web-ui == 'true'
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
tar -xf build.tar -C data/static
|
# tar -xf build.tar -C data/static
|
||||||
rm build.tar
|
# rm build.tar
|
||||||
|
|
||||||
- name: Remove debug flags for release
|
- name: Remove debug flags for release
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
2
.github/actions/setup-base/action.yml
vendored
2
.github/actions/setup-base/action.yml
vendored
@@ -5,7 +5,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
|||||||
2
.github/workflows/build_debian_src.yml
vendored
2
.github/workflows/build_debian_src.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
path: meshtasticd
|
path: meshtasticd
|
||||||
|
|||||||
37
.github/workflows/build_esp32.yml
vendored
37
.github/workflows/build_esp32.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Build ESP32
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-esp32:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build ESP32
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
remove-debug-flags: >-
|
|
||||||
./arch/esp32/esp32.ini
|
|
||||||
./arch/esp32/esp32s2.ini
|
|
||||||
./arch/esp32/esp32s3.ini
|
|
||||||
./arch/esp32/esp32c3.ini
|
|
||||||
./arch/esp32/esp32c6.ini
|
|
||||||
build-script-path: bin/build-esp32.sh
|
|
||||||
ota-firmware-source: firmware.bin
|
|
||||||
ota-firmware-target: release/bleota.bin
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.bin
|
|
||||||
release/*.elf
|
|
||||||
include-web-ui: true
|
|
||||||
arch: esp32
|
|
||||||
37
.github/workflows/build_esp32_c3.yml
vendored
37
.github/workflows/build_esp32_c3.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Build ESP32-C3
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-esp32-c3:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build ESP32-C3
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
remove-debug-flags: >-
|
|
||||||
./arch/esp32/esp32.ini
|
|
||||||
./arch/esp32/esp32s2.ini
|
|
||||||
./arch/esp32/esp32s3.ini
|
|
||||||
./arch/esp32/esp32c3.ini
|
|
||||||
./arch/esp32/esp32c6.ini
|
|
||||||
build-script-path: bin/build-esp32.sh
|
|
||||||
ota-firmware-source: firmware-c3.bin
|
|
||||||
ota-firmware-target: release/bleota-c3.bin
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.bin
|
|
||||||
release/*.elf
|
|
||||||
include-web-ui: true
|
|
||||||
arch: esp32c3
|
|
||||||
37
.github/workflows/build_esp32_c6.yml
vendored
37
.github/workflows/build_esp32_c6.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Build ESP32-C6
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-esp32-c6:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build ESP32-C6
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
remove-debug-flags: >-
|
|
||||||
./arch/esp32/esp32.ini
|
|
||||||
./arch/esp32/esp32s2.ini
|
|
||||||
./arch/esp32/esp32s3.ini
|
|
||||||
./arch/esp32/esp32c3.ini
|
|
||||||
./arch/esp32/esp32c6.ini
|
|
||||||
build-script-path: bin/build-esp32.sh
|
|
||||||
ota-firmware-source: firmware-c3.bin
|
|
||||||
ota-firmware-target: release/bleota-c3.bin
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.bin
|
|
||||||
release/*.elf
|
|
||||||
include-web-ui: true
|
|
||||||
arch: esp32c6
|
|
||||||
37
.github/workflows/build_esp32_s3.yml
vendored
37
.github/workflows/build_esp32_s3.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Build ESP32-S3
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-esp32-s3:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build ESP32-S3
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
remove-debug-flags: >-
|
|
||||||
./arch/esp32/esp32.ini
|
|
||||||
./arch/esp32/esp32s2.ini
|
|
||||||
./arch/esp32/esp32s3.ini
|
|
||||||
./arch/esp32/esp32c3.ini
|
|
||||||
./arch/esp32/esp32c6.ini
|
|
||||||
build-script-path: bin/build-esp32.sh
|
|
||||||
ota-firmware-source: firmware-s3.bin
|
|
||||||
ota-firmware-target: release/bleota-s3.bin
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.bin
|
|
||||||
release/*.elf
|
|
||||||
include-web-ui: true
|
|
||||||
arch: esp32s3
|
|
||||||
66
.github/workflows/build_firmware.yml
vendored
Normal file
66
.github/workflows/build_firmware.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
platform:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
pio_env:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pio-build:
|
||||||
|
name: build-${{ inputs.platform }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
- name: Set OTA firmware source and target
|
||||||
|
if: startsWith(inputs.platform, 'esp32')
|
||||||
|
id: ota_dir
|
||||||
|
env:
|
||||||
|
PIO_PLATFORM: ${{ inputs.platform }}
|
||||||
|
run: |
|
||||||
|
if [ "$PIO_PLATFORM" = "esp32s3" ]; then
|
||||||
|
echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then
|
||||||
|
echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$PIO_PLATFORM" = "esp32" ]; then
|
||||||
|
echo "src=firmware.bin" >> $GITHUB_OUTPUT
|
||||||
|
echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build ${{ inputs.platform }}
|
||||||
|
id: build
|
||||||
|
uses: meshtastic/gh-action-firmware@main
|
||||||
|
with:
|
||||||
|
pio_platform: ${{ inputs.platform }}
|
||||||
|
pio_env: ${{ inputs.pio_env }}
|
||||||
|
pio_target: build
|
||||||
|
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
|
||||||
|
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
|
||||||
|
|
||||||
|
- name: Store binaries as an artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
|
||||||
|
overwrite: true
|
||||||
|
path: |
|
||||||
|
release/*.bin
|
||||||
|
release/*.elf
|
||||||
|
release/*.uf2
|
||||||
|
release/*.hex
|
||||||
|
release/*-ota.zip
|
||||||
30
.github/workflows/build_nrf52.yml
vendored
30
.github/workflows/build_nrf52.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Build NRF52
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-nrf52:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build NRF52
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
build-script-path: bin/build-nrf52.sh
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.hex
|
|
||||||
release/*.uf2
|
|
||||||
release/*.elf
|
|
||||||
release/*.zip
|
|
||||||
arch: nrf52840
|
|
||||||
28
.github/workflows/build_rpi2040.yml
vendored
28
.github/workflows/build_rpi2040.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Build RPI2040
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-rpi2040:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build Raspberry Pi 2040
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
build-script-path: bin/build-rpi2040.sh
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.uf2
|
|
||||||
release/*.elf
|
|
||||||
arch: rp2040
|
|
||||||
29
.github/workflows/build_stm32.yml
vendored
29
.github/workflows/build_stm32.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: Build STM32
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
board:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions: read-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-stm32:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build STM32WL
|
|
||||||
id: build
|
|
||||||
uses: ./.github/actions/build-variant
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
board: ${{ inputs.board }}
|
|
||||||
build-script-path: bin/build-stm32.sh
|
|
||||||
artifact-paths: |
|
|
||||||
release/*.hex
|
|
||||||
release/*.bin
|
|
||||||
release/*.elf
|
|
||||||
arch: stm32
|
|
||||||
6
.github/workflows/daily_packaging.yml
vendored
6
.github/workflows/daily_packaging.yml
vendored
@@ -30,7 +30,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
series: [plucky, oracular, noble, jammy]
|
series:
|
||||||
|
- jammy # 22.04
|
||||||
|
- noble # 24.04
|
||||||
|
- plucky # 25.04
|
||||||
|
- questing # 25.10
|
||||||
uses: ./.github/workflows/package_ppa.yml
|
uses: ./.github/workflows/package_ppa.yml
|
||||||
with:
|
with:
|
||||||
ppa_repo: ppa:meshtastic/daily
|
ppa_repo: ppa:meshtastic/daily
|
||||||
|
|||||||
2
.github/workflows/docker_build.yml
vendored
2
.github/workflows/docker_build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
runs-on: ${{ inputs.runs-on }}
|
runs-on: ${{ inputs.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
|||||||
2
.github/workflows/docker_manifest.yml
vendored
2
.github/workflows/docker_manifest.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
|||||||
2
.github/workflows/hook_copr.yml
vendored
2
.github/workflows/hook_copr.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
|
|||||||
254
.github/workflows/main_matrix.yml
vendored
254
.github/workflows/main_matrix.yml
vendored
@@ -30,18 +30,31 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check]
|
arch:
|
||||||
runs-on: ubuntu-latest
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
|
- check
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- id: checkout
|
- uses: actions/checkout@v5
|
||||||
uses: actions/checkout@v4
|
- uses: actions/setup-python@v5
|
||||||
name: Checkout base
|
with:
|
||||||
- id: jsonStep
|
python-version: 3.x
|
||||||
|
cache: pip
|
||||||
|
- run: pip install -U platformio
|
||||||
|
- name: Generate matrix
|
||||||
|
id: jsonStep
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
|
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
|
||||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
|
||||||
else
|
else
|
||||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} quick)
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
|
||||||
fi
|
fi
|
||||||
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
|
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
|
||||||
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
|
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
|
||||||
@@ -52,9 +65,25 @@ jobs:
|
|||||||
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
|
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
|
||||||
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
|
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
|
||||||
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
|
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
|
||||||
|
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
|
||||||
stm32: ${{ steps.jsonStep.outputs.stm32 }}
|
stm32: ${{ steps.jsonStep.outputs.stm32 }}
|
||||||
check: ${{ steps.jsonStep.outputs.check }}
|
check: ${{ steps.jsonStep.outputs.check }}
|
||||||
|
|
||||||
|
version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Get release version string
|
||||||
|
run: |
|
||||||
|
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||||
|
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
BUILD_LOCATION: local
|
||||||
|
outputs:
|
||||||
|
long: ${{ steps.version.outputs.long }}
|
||||||
|
deb: ${{ steps.version.outputs.deb }}
|
||||||
|
|
||||||
check:
|
check:
|
||||||
needs: setup
|
needs: setup
|
||||||
strategy:
|
strategy:
|
||||||
@@ -64,7 +93,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Build base
|
- name: Build base
|
||||||
id: base
|
id: base
|
||||||
uses: ./.github/actions/setup-base
|
uses: ./.github/actions/setup-base
|
||||||
@@ -72,69 +101,95 @@ jobs:
|
|||||||
run: bin/check-all.sh ${{ matrix.board }}
|
run: bin/check-all.sh ${{ matrix.board }}
|
||||||
|
|
||||||
build-esp32:
|
build-esp32:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
|
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
|
||||||
uses: ./.github/workflows/build_esp32.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: esp32
|
||||||
|
|
||||||
build-esp32-s3:
|
build-esp32s3:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
|
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
|
||||||
uses: ./.github/workflows/build_esp32_s3.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: esp32s3
|
||||||
|
|
||||||
build-esp32-c3:
|
build-esp32c3:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
|
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
|
||||||
uses: ./.github/workflows/build_esp32_c3.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: esp32c3
|
||||||
|
|
||||||
build-esp32-c6:
|
build-esp32c6:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
|
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
|
||||||
uses: ./.github/workflows/build_esp32_c6.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: esp32c6
|
||||||
|
|
||||||
build-nrf52:
|
build-nrf52840:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
|
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
|
||||||
uses: ./.github/workflows/build_nrf52.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: nrf52840
|
||||||
|
|
||||||
build-rpi2040:
|
build-rp2040:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
|
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
|
||||||
uses: ./.github/workflows/build_rpi2040.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: rp2040
|
||||||
|
|
||||||
|
build-rp2350:
|
||||||
|
needs: [setup, version]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
|
||||||
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: rp2350
|
||||||
|
|
||||||
build-stm32:
|
build-stm32:
|
||||||
needs: setup
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
|
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
|
||||||
uses: ./.github/workflows/build_stm32.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
board: ${{ matrix.board }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.board }}
|
||||||
|
platform: stm32
|
||||||
|
|
||||||
build-debian-src:
|
build-debian-src:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
uses: ./.github/workflows/build_debian_src.yml
|
uses: ./.github/workflows/build_debian_src.yml
|
||||||
with:
|
with:
|
||||||
series: UNRELEASED
|
series: UNRELEASED
|
||||||
@@ -209,26 +264,36 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
|
arch:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
[
|
[
|
||||||
|
version,
|
||||||
build-esp32,
|
build-esp32,
|
||||||
build-esp32-s3,
|
build-esp32s3,
|
||||||
build-esp32-c3,
|
build-esp32c3,
|
||||||
build-esp32-c6,
|
build-esp32c6,
|
||||||
build-nrf52,
|
build-nrf52840,
|
||||||
build-rpi2040,
|
build-rp2040,
|
||||||
|
build-rp2350,
|
||||||
build-stm32,
|
build-stm32,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: ./
|
path: ./
|
||||||
pattern: firmware-${{matrix.arch}}-*
|
pattern: firmware-${{matrix.arch}}-*
|
||||||
@@ -237,17 +302,13 @@ jobs:
|
|||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Get release version string
|
|
||||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- name: Move files up
|
- name: Move files up
|
||||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||||
|
|
||||||
- name: Repackage in single firmware zip
|
- name: Repackage in single firmware zip
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
path: |
|
path: |
|
||||||
./firmware-*.bin
|
./firmware-*.bin
|
||||||
@@ -257,14 +318,13 @@ jobs:
|
|||||||
./device-*.sh
|
./device-*.sh
|
||||||
./device-*.bat
|
./device-*.bat
|
||||||
./littlefs-*.bin
|
./littlefs-*.bin
|
||||||
./littlefswebui-*.bin
|
|
||||||
./bleota*bin
|
./bleota*bin
|
||||||
./Meshtastic_nRF52_factory_erase*.uf2
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output
|
path: ./output
|
||||||
|
|
||||||
@@ -278,12 +338,12 @@ jobs:
|
|||||||
chmod +x ./output/device-update.sh
|
chmod +x ./output/device-update.sh
|
||||||
|
|
||||||
- name: Zip firmware
|
- name: Zip firmware
|
||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- name: Repackage in single elfs zip
|
- name: Repackage in single elfs zip
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
overwrite: true
|
overwrite: true
|
||||||
path: ./*.elf
|
path: ./*.elf
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
@@ -291,8 +351,8 @@ jobs:
|
|||||||
- uses: scruplelesswizard/comment-artifact@main
|
- uses: scruplelesswizard/comment-artifact@main
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
description: "Download firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
release-artifacts:
|
release-artifacts:
|
||||||
@@ -301,56 +361,49 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
needs:
|
needs:
|
||||||
|
- version
|
||||||
- gather-artifacts
|
- gather-artifacts
|
||||||
- build-debian-src
|
- build-debian-src
|
||||||
- package-pio-deps-native-tft
|
- package-pio-deps-native-tft
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- name: Get release version string
|
|
||||||
run: |
|
|
||||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
||||||
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
env:
|
|
||||||
BUILD_LOCATION: local
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
id: create_release
|
id: create_release
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
name: Meshtastic Firmware ${{ steps.version.outputs.long }} Alpha
|
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||||
tag_name: v${{ steps.version.outputs.long }}
|
tag_name: v${{ needs.version.outputs.long }}
|
||||||
body: |
|
body: |
|
||||||
Autogenerated by github action, developer should edit as required before publishing...
|
Autogenerated by github action, developer should edit as required before publishing...
|
||||||
|
|
||||||
- name: Download source deb
|
- name: Download source deb
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: firmware-debian-${{ steps.version.outputs.deb }}~UNRELEASED-src
|
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/debian-src
|
path: ./output/debian-src
|
||||||
|
|
||||||
- name: Download `native-tft` pio deps
|
- name: Download `native-tft` pio deps
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
|
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output/pio-deps-native-tft
|
path: ./output/pio-deps-native-tft
|
||||||
|
|
||||||
- name: Zip Linux sources
|
- name: Zip Linux sources
|
||||||
working-directory: output
|
working-directory: output
|
||||||
run: |
|
run: |
|
||||||
zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src
|
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
|
||||||
zip -9 -r ./platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip ./pio-deps-native-tft
|
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
|
||||||
|
|
||||||
# For diagnostics
|
# For diagnostics
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
@@ -360,8 +413,8 @@ jobs:
|
|||||||
# Only run when targeting master branch with workflow_dispatch
|
# Only run when targeting master branch with workflow_dispatch
|
||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
run: |
|
run: |
|
||||||
gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip
|
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
|
||||||
gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip
|
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -369,26 +422,30 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
|
arch:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
needs: [release-artifacts]
|
needs: [release-artifacts, version]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- name: Get release version string
|
- uses: actions/download-artifact@v5
|
||||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
pattern: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
|
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./output
|
path: ./output
|
||||||
|
|
||||||
@@ -401,16 +458,16 @@ jobs:
|
|||||||
chmod +x ./output/device-update.sh
|
chmod +x ./output/device-update.sh
|
||||||
|
|
||||||
- name: Zip firmware
|
- name: Zip firmware
|
||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./elfs
|
path: ./elfs
|
||||||
|
|
||||||
- name: Zip debug elfs
|
- name: Zip debug elfs
|
||||||
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./elfs
|
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
|
||||||
|
|
||||||
# For diagnostics
|
# For diagnostics
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
@@ -420,33 +477,30 @@ jobs:
|
|||||||
# Only run when targeting master branch with workflow_dispatch
|
# Only run when targeting master branch with workflow_dispatch
|
||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
run: |
|
run: |
|
||||||
gh release upload v${{ steps.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
|
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
gh release upload v${{ steps.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
|
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
publish-firmware:
|
publish-firmware:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
needs: [release-firmware]
|
needs: [release-firmware, version]
|
||||||
env:
|
env:
|
||||||
targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32
|
targets: |-
|
||||||
|
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- name: Get release version string
|
- uses: actions/download-artifact@v5
|
||||||
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
||||||
id: version
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
pattern: firmware-{${{ env.targets }}}-${{ steps.version.outputs.long }}
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: ./publish
|
path: ./publish
|
||||||
|
|
||||||
@@ -460,9 +514,9 @@ jobs:
|
|||||||
external_repository: meshtastic/meshtastic.github.io
|
external_repository: meshtastic/meshtastic.github.io
|
||||||
publish_branch: master
|
publish_branch: master
|
||||||
publish_dir: ./publish
|
publish_dir: ./publish
|
||||||
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ steps.version.outputs.long }}
|
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
|
||||||
keep_files: true
|
keep_files: true
|
||||||
user_name: github-actions[bot]
|
user_name: github-actions[bot]
|
||||||
user_email: github-actions[bot]@users.noreply.github.com
|
user_email: github-actions[bot]@users.noreply.github.com
|
||||||
commit_message: ${{ steps.version.outputs.long }}
|
commit_message: ${{ needs.version.outputs.long }}
|
||||||
enable_jekyll: true
|
enable_jekyll: true
|
||||||
|
|||||||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
@@ -8,12 +8,13 @@ permissions: read-all
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trunk_check:
|
trunk_check:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
name: Trunk Check and Upload
|
name: Trunk Check and Upload
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Trunk Check
|
- name: Trunk Check
|
||||||
uses: trunk-io/trunk-action@v1
|
uses: trunk-io/trunk-action@v1
|
||||||
@@ -21,6 +22,7 @@ jobs:
|
|||||||
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
trunk-token: ${{ secrets.TRUNK_TOKEN }}
|
||||||
|
|
||||||
trunk_upgrade:
|
trunk_upgrade:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
|
||||||
name: Trunk Upgrade (PR)
|
name: Trunk Upgrade (PR)
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -29,7 +31,7 @@ jobs:
|
|||||||
pull-requests: write # For trunk to create PRs
|
pull-requests: write # For trunk to create PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Trunk Upgrade
|
- name: Trunk Upgrade
|
||||||
uses: trunk-io/trunk-action/upgrade@v1
|
uses: trunk-io/trunk-action/upgrade@v1
|
||||||
|
|||||||
4
.github/workflows/package_obs.yml
vendored
4
.github/workflows/package_obs.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
needs: build-debian-src
|
needs: build-debian-src
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
path: meshtasticd
|
path: meshtasticd
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
2
.github/workflows/package_pio_deps.yml
vendored
2
.github/workflows/package_pio_deps.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
|||||||
4
.github/workflows/package_ppa.yml
vendored
4
.github/workflows/package_ppa.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
needs: build-debian-src
|
needs: build-debian-src
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
path: meshtasticd
|
path: meshtasticd
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
24
.github/workflows/pr_enforce_labels.yml
vendored
Normal file
24
.github/workflows/pr_enforce_labels.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Check PR Labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, labeled, unlabeled, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-label:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Check for PR labels
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const labels = context.payload.pull_request.labels.map(label => label.name);
|
||||||
|
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk'];
|
||||||
|
const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
|
||||||
|
if (!hasRequiredLabel) {
|
||||||
|
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
|
||||||
|
}
|
||||||
238
.github/workflows/pr_tests.yml
vendored
Normal file
238
.github/workflows/pr_tests.yml
vendored
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
# DISABLED: Changed from automatic PR triggers to manual only
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
reason:
|
||||||
|
description: "Reason for manual test run"
|
||||||
|
required: false
|
||||||
|
default: "Manual test execution"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: tests-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
native-tests:
|
||||||
|
name: "🧪 Native Tests"
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
|
||||||
|
test-summary:
|
||||||
|
name: "📊 Test Results"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [native-tests]
|
||||||
|
if: always()
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Get release version string
|
||||||
|
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||||
|
id: version
|
||||||
|
|
||||||
|
- name: Download test artifacts
|
||||||
|
if: needs.native-tests.result != 'skipped'
|
||||||
|
uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Parse test results and create detailed summary
|
||||||
|
id: test-results
|
||||||
|
run: |
|
||||||
|
echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check overall job status first
|
||||||
|
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||||
|
echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then
|
||||||
|
echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then
|
||||||
|
echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Parse detailed test results if available
|
||||||
|
if [ -f "testreport.xml" ]; then
|
||||||
|
echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
python3 << 'EOF'
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ET.parse('testreport.xml')
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
total_tests = 0
|
||||||
|
passed_tests = 0
|
||||||
|
failed_tests = 0
|
||||||
|
skipped_tests = 0
|
||||||
|
|
||||||
|
# Parse testsuite elements
|
||||||
|
for testsuite in root.findall('.//testsuite'):
|
||||||
|
suite_name = testsuite.get('name', 'Unknown')
|
||||||
|
suite_tests = int(testsuite.get('tests', '0'))
|
||||||
|
suite_failures = int(testsuite.get('failures', '0'))
|
||||||
|
suite_errors = int(testsuite.get('errors', '0'))
|
||||||
|
suite_skipped = int(testsuite.get('skipped', '0'))
|
||||||
|
|
||||||
|
total_tests += suite_tests
|
||||||
|
failed_tests += suite_failures + suite_errors
|
||||||
|
skipped_tests += suite_skipped
|
||||||
|
passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped
|
||||||
|
|
||||||
|
if suite_tests > 0:
|
||||||
|
status = "✅" if (suite_failures + suite_errors) == 0 else "❌"
|
||||||
|
print(f"**{status} Test Suite: {suite_name}**")
|
||||||
|
print(f"- Total: {suite_tests}")
|
||||||
|
print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}")
|
||||||
|
print(f"- Failed: ❌ {suite_failures + suite_errors}")
|
||||||
|
if suite_skipped > 0:
|
||||||
|
print(f"- Skipped: ⏭️ {suite_skipped}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# Show individual test results for failed suites
|
||||||
|
if suite_failures + suite_errors > 0:
|
||||||
|
print("**Failed Tests:**")
|
||||||
|
for testcase in testsuite.findall('testcase'):
|
||||||
|
test_name = testcase.get('name', 'Unknown')
|
||||||
|
failure = testcase.find('failure')
|
||||||
|
error = testcase.find('error')
|
||||||
|
|
||||||
|
if failure is not None:
|
||||||
|
msg = failure.get('message', 'Unknown error')[:100]
|
||||||
|
print(f"- ❌ `{test_name}`: {msg}")
|
||||||
|
elif error is not None:
|
||||||
|
msg = error.get('message', 'Unknown error')[:100]
|
||||||
|
print(f"- ❌ `{test_name}`: ERROR - {msg}")
|
||||||
|
print("")
|
||||||
|
else:
|
||||||
|
# Show passed tests for successful suites
|
||||||
|
passed_count = 0
|
||||||
|
for testcase in testsuite.findall('testcase'):
|
||||||
|
if testcase.find('failure') is None and testcase.find('error') is None:
|
||||||
|
if passed_count < 5: # Limit to first 5 to avoid spam
|
||||||
|
test_name = testcase.get('name', 'Unknown')
|
||||||
|
print(f"- ✅ `{test_name}`: PASSED")
|
||||||
|
passed_count += 1
|
||||||
|
if passed_count > 5:
|
||||||
|
print(f"- ... and {passed_count - 5} more tests passed")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
print("### 📊 Test Statistics")
|
||||||
|
print(f"- **Total Tests**: {total_tests}")
|
||||||
|
print(f"- **Passed**: ✅ {passed_tests}")
|
||||||
|
print(f"- **Failed**: ❌ {failed_tests}")
|
||||||
|
if skipped_tests > 0:
|
||||||
|
print(f"- **Skipped**: ⏭️ {skipped_tests}")
|
||||||
|
|
||||||
|
if failed_tests > 0:
|
||||||
|
print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**")
|
||||||
|
else:
|
||||||
|
print(f"\n✅ **All {total_tests} tests passed!**")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error parsing test results: {e}")
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Comment test results on PR
|
||||||
|
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Read the step summary to use as PR comment
|
||||||
|
let testSummary = "## 🧪 Test Results Summary\n\n";
|
||||||
|
|
||||||
|
if ("${{ needs.native-tests.result }}" === "success") {
|
||||||
|
testSummary += "✅ **All tests passed!**\n\n";
|
||||||
|
} else if ("${{ needs.native-tests.result }}" === "failure") {
|
||||||
|
testSummary += "❌ **Some tests failed.**\n\n";
|
||||||
|
} else {
|
||||||
|
testSummary += "⚠️ **Tests did not complete normally.**\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`;
|
||||||
|
testSummary += "---\n";
|
||||||
|
testSummary += "*This comment will be automatically updated when new commits are pushed.*";
|
||||||
|
|
||||||
|
// Find existing comment
|
||||||
|
const comments = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number
|
||||||
|
});
|
||||||
|
|
||||||
|
const botComment = comments.data.find(comment =>
|
||||||
|
comment.user.type === 'Bot' &&
|
||||||
|
comment.body.includes('🧪 Test Results Summary')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (botComment) {
|
||||||
|
// Update existing comment
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: botComment.id,
|
||||||
|
body: testSummary
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body: testSummary
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Set overall status
|
||||||
|
run: |
|
||||||
|
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
|
||||||
|
echo "All tests passed! ✅"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Some tests failed! ❌"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
11
.github/workflows/release_channels.yml
vendored
11
.github/workflows/release_channels.yml
vendored
@@ -20,7 +20,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
series: [plucky, oracular, noble, jammy]
|
series:
|
||||||
|
- jammy # 22.04
|
||||||
|
- noble # 24.04
|
||||||
|
- plucky # 25.04
|
||||||
|
# - questing # 25.10
|
||||||
uses: ./.github/workflows/package_ppa.yml
|
uses: ./.github/workflows/package_ppa.yml
|
||||||
with:
|
with:
|
||||||
ppa_repo: |-
|
ppa_repo: |-
|
||||||
@@ -56,7 +60,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -99,8 +103,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
base: ${{ github.event.repository.default_branch }}
|
base: ${{ github.event.repository.default_branch }}
|
||||||
branch: create-pull-request/bump-version
|
branch: create-pull-request/bump-version
|
||||||
|
labels: github_actions
|
||||||
title: Bump release version
|
title: Bump release version
|
||||||
commit-message: automated bumps
|
commit-message: Automated version bumps
|
||||||
add-paths: |
|
add-paths: |
|
||||||
version.properties
|
version.properties
|
||||||
debian/changelog
|
debian/changelog
|
||||||
|
|||||||
3
.github/workflows/sec_sast_semgrep_cron.yml
vendored
3
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@@ -13,6 +13,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
semgrep-full:
|
semgrep-full:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
container:
|
container:
|
||||||
image: semgrep/semgrep
|
image: semgrep/semgrep
|
||||||
@@ -20,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# step 1
|
# step 1
|
||||||
- name: clone application source code
|
- name: clone application source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# step 2
|
# step 2
|
||||||
- name: full scan
|
- name: full scan
|
||||||
|
|||||||
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
2
.github/workflows/sec_sast_semgrep_pull.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# step 1
|
# step 1
|
||||||
- name: clone application source code
|
- name: clone application source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/stale_bot.yml
vendored
1
.github/workflows/stale_bot.yml
vendored
@@ -11,6 +11,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale_issues:
|
stale_issues:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
name: Close Stale Issues
|
name: Close Stale Issues
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/test_native.yml
vendored
12
.github/workflows/test_native.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
name: Native Simulator Tests
|
name: Native Simulator Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
name: Native PlatformIO Tests
|
name: Native PlatformIO Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
- platformio-tests
|
- platformio-tests
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
@@ -137,20 +137,20 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
|
|
||||||
- name: Download test artifacts
|
- name: Download test artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Test Report
|
- name: Test Report
|
||||||
uses: dorny/test-reporter@v2.1.0
|
uses: dorny/test-reporter@v2.1.1
|
||||||
with:
|
with:
|
||||||
name: PlatformIO Tests
|
name: PlatformIO Tests
|
||||||
path: testreport.xml
|
path: testreport.xml
|
||||||
reporter: java-junit
|
reporter: java-junit
|
||||||
|
|
||||||
- name: Download coverage artifacts
|
- name: Download coverage artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
|
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
|
||||||
path: code-coverage-report
|
path: code-coverage-report
|
||||||
|
|||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -12,13 +12,15 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
native-tests:
|
native-tests:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
uses: ./.github/workflows/test_native.yml
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
hardware-tests:
|
hardware-tests:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
runs-on: test-runner
|
runs-on: test-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# - uses: actions/setup-python@v5
|
# - uses: actions/setup-python@v5
|
||||||
# with:
|
# with:
|
||||||
|
|||||||
2
.github/workflows/trunk_annotate_pr.yml
vendored
2
.github/workflows/trunk_annotate_pr.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Trunk Check
|
- name: Trunk Check
|
||||||
uses: trunk-io/trunk-action@v1
|
uses: trunk-io/trunk-action@v1
|
||||||
|
|||||||
2
.github/workflows/trunk_check.yml
vendored
2
.github/workflows/trunk_check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Trunk Check
|
- name: Trunk Check
|
||||||
uses: trunk-io/trunk-action@v1
|
uses: trunk-io/trunk-action@v1
|
||||||
|
|||||||
2
.github/workflows/trunk_format_pr.yml
vendored
2
.github/workflows/trunk_format_pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{github.event.pull_request.head.ref}}
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|||||||
4
.github/workflows/update_protobufs.yml
vendored
4
.github/workflows/update_protobufs.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ jobs:
|
|||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
branch: create-pull-request/update-protobufs
|
branch: create-pull-request/update-protobufs
|
||||||
|
labels: submodules
|
||||||
title: Update protobufs and classes
|
title: Update protobufs and classes
|
||||||
|
commit-message: Update protobufs
|
||||||
add-paths: |
|
add-paths: |
|
||||||
protobufs
|
protobufs
|
||||||
src/mesh
|
src/mesh
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -37,4 +37,7 @@ release/
|
|||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
/compile_commands.json
|
/compile_commands.json
|
||||||
src/mesh/raspihttp/certificate.pem
|
src/mesh/raspihttp/certificate.pem
|
||||||
src/mesh/raspihttp/private_key.pem
|
src/mesh/raspihttp/private_key.pem
|
||||||
|
|
||||||
|
# Ignore logo (set at build time with platformio-custom.py)
|
||||||
|
data/boot/logo.*
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
version: 0.1
|
version: 0.1
|
||||||
cli:
|
cli:
|
||||||
version: 1.24.0
|
version: 1.25.0
|
||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.7.0
|
ref: v1.7.1
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.442
|
- checkov@3.2.461
|
||||||
- renovate@40.60.3
|
- renovate@41.74.0
|
||||||
- prettier@3.5.3
|
- prettier@3.6.2
|
||||||
- trufflehog@3.89.2
|
- trufflehog@3.90.5
|
||||||
- yamllint@1.37.1
|
- yamllint@1.37.1
|
||||||
- bandit@1.8.5
|
- bandit@1.8.6
|
||||||
- trivy@0.63.0
|
- trivy@0.64.1
|
||||||
- taplo@0.9.3
|
- taplo@0.9.3
|
||||||
- ruff@0.12.0
|
- ruff@0.12.7
|
||||||
- isort@6.0.1
|
- isort@6.0.1
|
||||||
- markdownlint@0.45.0
|
- markdownlint@0.45.0
|
||||||
- oxipng@9.1.5
|
- oxipng@9.1.5
|
||||||
- svgo@3.3.2
|
- svgo@4.0.0
|
||||||
- actionlint@1.7.7
|
- actionlint@1.7.7
|
||||||
- flake8@7.2.0
|
- flake8@7.3.0
|
||||||
- hadolint@2.12.1-beta
|
- hadolint@2.12.1-beta
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- shellcheck@0.10.0
|
- shellcheck@0.10.0
|
||||||
- black@25.1.0
|
- black@25.1.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.27.2
|
- gitleaks@8.28.0
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
ignore:
|
ignore:
|
||||||
- linters: [ALL]
|
- linters: [ALL]
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -3,7 +3,7 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM python:3.13-bookworm AS builder
|
FROM python:3.13-slim-trixie AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV TZ=Etc/UTC
|
ENV TZ=Etc/UTC
|
||||||
@@ -36,7 +36,7 @@ RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/fir
|
|||||||
|
|
||||||
##### PRODUCTION BUILD #############
|
##### PRODUCTION BUILD #############
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
LABEL org.opencontainers.image.title="Meshtastic" \
|
LABEL org.opencontainers.image.title="Meshtastic" \
|
||||||
org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \
|
org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \
|
||||||
org.opencontainers.image.url="https://meshtastic.org" \
|
org.opencontainers.image.url="https://meshtastic.org" \
|
||||||
@@ -51,8 +51,8 @@ ENV TZ=Etc/UTC
|
|||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get update && apt-get --no-install-recommends -y install \
|
RUN apt-get update && apt-get --no-install-recommends -y install \
|
||||||
libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \
|
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
|
||||||
liborcania2.3 libulfius2.7 libssl3 \
|
liborcania2.3 libulfius2.7t64 libssl3t64 \
|
||||||
libx11-6 libinput10 libxkbcommon-x11-0 \
|
libx11-6 libinput10 libxkbcommon-x11-0 \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
|
||||||
&& mkdir -p /var/lib/meshtasticd \
|
&& mkdir -p /var/lib/meshtasticd \
|
||||||
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \
|
|||||||
|
|
||||||
# Fetch compiled binary from the builder
|
# Fetch compiled binary from the builder
|
||||||
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
|
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
|
||||||
COPY --from=builder /tmp/web /usr/share/meshtasticd/
|
COPY --from=builder /tmp/web /usr/share/meshtasticd/web/
|
||||||
# Copy config templates
|
# Copy config templates
|
||||||
COPY ./bin/config.d /etc/meshtasticd/available.d
|
COPY ./bin/config.d /etc/meshtasticd/available.d
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ lib_deps =
|
|||||||
${environmental_extra.lib_deps}
|
${environmental_extra.lib_deps}
|
||||||
${radiolib_base.lib_deps}
|
${radiolib_base.lib_deps}
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master
|
||||||
https://github.com/meshtastic/esp32_https_server/archive/896f1771ceb5979987a0b41028bf1b4e7aad419b.zip
|
https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip
|
||||||
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
|
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
|
||||||
h2zero/NimBLE-Arduino@^1.4.3
|
h2zero/NimBLE-Arduino@^1.4.3
|
||||||
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
|
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
|
||||||
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
|
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
|
||||||
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
|
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
|
||||||
lewisxhe/XPowersLib@^0.2.7
|
https://github.com/lewisxhe/XPowersLib/archive/v0.3.0.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
|
||||||
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
|
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
|
||||||
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ lib_deps =
|
|||||||
${environmental_extra.lib_deps}
|
${environmental_extra.lib_deps}
|
||||||
${radiolib_base.lib_deps}
|
${radiolib_base.lib_deps}
|
||||||
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
|
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
|
||||||
lewisxhe/XPowersLib@^0.2.7
|
lewisxhe/XPowersLib@0.3.0
|
||||||
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
|
||||||
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
|
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
|
||||||
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ build_flags =
|
|||||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
|
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> -<serialization/>
|
||||||
|
|
||||||
lib_deps=
|
lib_deps=
|
||||||
${arduino_base.lib_deps}
|
${arduino_base.lib_deps}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
[portduino_base]
|
[portduino_base]
|
||||||
platform =
|
platform =
|
||||||
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
|
||||||
https://github.com/meshtastic/platform-native/archive/681ee029207e9fd040afa223df6e54074cbbe084.zip
|
https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
@@ -17,7 +17,6 @@ build_src_filter =
|
|||||||
+<mesh/raspihttp/>
|
+<mesh/raspihttp/>
|
||||||
-<mesh/eth/>
|
-<mesh/eth/>
|
||||||
-<modules/esp32>
|
-<modules/esp32>
|
||||||
+<../variants/portduino>
|
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${env.lib_deps}
|
${env.lib_deps}
|
||||||
@@ -30,14 +29,17 @@ lib_deps =
|
|||||||
lovyan03/LovyanGFX@^1.2.0
|
lovyan03/LovyanGFX@^1.2.0
|
||||||
# renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
|
# renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
|
||||||
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
|
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
|
||||||
|
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
|
||||||
|
adafruit/Adafruit seesaw Library@1.7.9
|
||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
${arduino_base.build_flags}
|
${arduino_base.build_flags}
|
||||||
|
-D ARCH_PORTDUINO
|
||||||
-fPIC
|
-fPIC
|
||||||
-Isrc/platform/portduino
|
-Isrc/platform/portduino
|
||||||
-DRADIOLIB_EEPROM_UNSUPPORTED
|
-DRADIOLIB_EEPROM_UNSUPPORTED
|
||||||
-DPORTDUINO_LINUX_HARDWARE
|
-DPORTDUINO_LINUX_HARDWARE
|
||||||
-DHAS_UDP_MULTICAST
|
-DHAS_UDP_MULTICAST=1
|
||||||
-lpthread
|
-lpthread
|
||||||
-lstdc++fs
|
-lstdc++fs
|
||||||
-lbluetooth
|
-lbluetooth
|
||||||
@@ -48,4 +50,7 @@ build_flags =
|
|||||||
-std=gnu17
|
-std=gnu17
|
||||||
-std=c++17
|
-std=c++17
|
||||||
|
|
||||||
lib_ignore = Adafruit NeoPixel
|
lib_ignore =
|
||||||
|
Adafruit NeoPixel
|
||||||
|
Adafruit ST7735 and ST7789 Library
|
||||||
|
SD
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
extends = arduino_base
|
extends = arduino_base
|
||||||
platform =
|
platform =
|
||||||
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
|
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
|
||||||
platformio/ststm32@19.2.0
|
platformio/ststm32@19.3.0
|
||||||
platform_packages =
|
platform_packages =
|
||||||
# TODO renovate
|
# TODO renovate
|
||||||
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
|
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
|
||||||
@@ -16,18 +16,27 @@ build_flags =
|
|||||||
${arduino_base.build_flags}
|
${arduino_base.build_flags}
|
||||||
-flto
|
-flto
|
||||||
-Isrc/platform/stm32wl -g
|
-Isrc/platform/stm32wl -g
|
||||||
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
-DMESHTASTIC_EXCLUDE_AUDIO=1
|
||||||
-DMESHTASTIC_EXCLUDE_INPUTBROKER
|
-DMESHTASTIC_EXCLUDE_ATAK=1 ; ATAK is quite big, disable it for big flash savings.
|
||||||
-DMESHTASTIC_EXCLUDE_I2C
|
-DMESHTASTIC_EXCLUDE_INPUTBROKER=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERMON
|
-DMESHTASTIC_EXCLUDE_POWERMON=1
|
||||||
-DMESHTASTIC_EXCLUDE_SCREEN
|
-DMESHTASTIC_EXCLUDE_SCREEN=1
|
||||||
-DMESHTASTIC_EXCLUDE_MQTT
|
-DMESHTASTIC_EXCLUDE_MQTT=1
|
||||||
-DMESHTASTIC_EXCLUDE_BLUETOOTH
|
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1
|
||||||
-DMESHTASTIC_EXCLUDE_GPS
|
-DMESHTASTIC_EXCLUDE_WIFI=1
|
||||||
;-DDEBUG_MUTE
|
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
|
||||||
|
-DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small.
|
||||||
|
-DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported.
|
||||||
|
-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized.
|
||||||
|
-DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs.
|
||||||
-fmerge-all-constants
|
-fmerge-all-constants
|
||||||
-ffunction-sections
|
-ffunction-sections
|
||||||
-fdata-sections
|
-fdata-sections
|
||||||
|
-DRADIOLIB_EXCLUDE_SX128X=1
|
||||||
|
-DRADIOLIB_EXCLUDE_SX127X=1
|
||||||
|
-DRADIOLIB_EXCLUDE_LR11X0=1
|
||||||
|
-DHAL_DAC_MODULE_ONLY
|
||||||
|
-DHAL_RNG_MODULE_ENABLED
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<mesh/api/> -<mesh/wifi/> -<mesh/http/> -<modules/esp32> -<mesh/eth/> -<input> -<buzz> -<modules/RemoteHardwareModule.cpp> -<platform/nrf52> -<platform/portduino> -<platform/rp2xx0> -<mesh/raspihttp>
|
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<mesh/api/> -<mesh/wifi/> -<mesh/http/> -<modules/esp32> -<mesh/eth/> -<input> -<buzz> -<modules/RemoteHardwareModule.cpp> -<platform/nrf52> -<platform/portduino> -<platform/rp2xx0> -<mesh/raspihttp>
|
||||||
@@ -39,9 +48,9 @@ debug_tool = stlink
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${env.lib_deps}
|
${env.lib_deps}
|
||||||
${radiolib_base.lib_deps}
|
${radiolib_base.lib_deps}
|
||||||
|
|
||||||
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
|
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
|
||||||
https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip
|
https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip
|
||||||
|
|
||||||
lib_ignore =
|
lib_ignore =
|
||||||
mathertel/OneButton@2.6.1
|
OneButton
|
||||||
Wire
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
platformio pkg update -e $1
|
platformio pkg install -e $1
|
||||||
|
|
||||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||||
rm -f .pio/build/$1/firmware.*
|
rm -f .pio/build/$1/firmware.*
|
||||||
@@ -34,11 +34,12 @@ SRCBIN=.pio/build/$1/firmware.bin
|
|||||||
cp $SRCBIN $OUTDIR/$basename-update.bin
|
cp $SRCBIN $OUTDIR/$basename-update.bin
|
||||||
|
|
||||||
echo "Building Filesystem for ESP32 targets"
|
echo "Building Filesystem for ESP32 targets"
|
||||||
pio run --environment $1 -t buildfs
|
# If you want to build the webui, uncomment the following lines
|
||||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
# pio run --environment $1 -t buildfs
|
||||||
# Remove webserver files from the filesystem and rebuild
|
# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
|
||||||
ls -l data/static # Diagnostic list of files
|
# # Remove webserver files from the filesystem and rebuild
|
||||||
rm -rf data/static
|
# ls -l data/static # Diagnostic list of files
|
||||||
|
# rm -rf data/static
|
||||||
pio run --environment $1 -t buildfs
|
pio run --environment $1 -t buildfs
|
||||||
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
|
||||||
cp bin/device-install.* $OUTDIR
|
cp bin/device-install.* $OUTDIR
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then
|
|||||||
elif (echo $2 | grep -q "stm32"); then
|
elif (echo $2 | grep -q "stm32"); then
|
||||||
bin/build-stm32.sh $1
|
bin/build-stm32.sh $1
|
||||||
elif (echo $2 | grep -q "rpi2040"); then
|
elif (echo $2 | grep -q "rpi2040"); then
|
||||||
bin/build-rpi2040.sh $1
|
bin/build-rp2xx0.sh $1
|
||||||
else
|
else
|
||||||
echo "Unknown target $2"
|
echo "Unknown target $2"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ mkdir -p $OUTDIR/
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
pio pkg update --environment "$PIO_ENV" || platformioFailed
|
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
||||||
pio run --environment "$PIO_ENV" || platformioFailed
|
pio run --environment "$PIO_ENV" || platformioFailed
|
||||||
cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||||
cp bin/native-install.* $OUTDIR
|
cp bin/native-install.* $OUTDIR
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
platformio pkg update -e $1
|
platformio pkg install -e $1
|
||||||
|
|
||||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||||
rm -f .pio/build/$1/firmware.*
|
rm -f .pio/build/$1/firmware.*
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
platformio pkg update -e $1
|
platformio pkg install -e $1
|
||||||
|
|
||||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||||
rm -f .pio/build/$1/firmware.*
|
rm -f .pio/build/$1/firmware.*
|
||||||
@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
|
|||||||
rm -r $OUTDIR/* || true
|
rm -r $OUTDIR/* || true
|
||||||
|
|
||||||
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
|
||||||
platformio pkg update -e $1
|
platformio pkg install -e $1
|
||||||
|
|
||||||
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
|
||||||
rm -f .pio/build/$1/firmware.*
|
rm -f .pio/build/$1/firmware.*
|
||||||
@@ -199,6 +199,10 @@ HostMetrics:
|
|||||||
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
|
||||||
|
|
||||||
|
|
||||||
|
Config:
|
||||||
|
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
|
||||||
|
# DisplayMode: COLOR # uncomment to force MUI
|
||||||
|
|
||||||
General:
|
General:
|
||||||
MaxNodes: 200
|
MaxNodes: 200
|
||||||
MaxMessageQueue: 100
|
MaxMessageQueue: 100
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ Input:
|
|||||||
TrackballLeft: 5
|
TrackballLeft: 5
|
||||||
TrackballRight: 26
|
TrackballRight: 26
|
||||||
TrackballPress: 13
|
TrackballPress: 13
|
||||||
|
TrackballDirection: FALLING
|
||||||
# User: 21
|
# User: 21
|
||||||
|
|||||||
@@ -9,13 +9,4 @@ Lora:
|
|||||||
DIO3_TCXO_VOLTAGE: true
|
DIO3_TCXO_VOLTAGE: true
|
||||||
DIO2_AS_RF_SWITCH: true
|
DIO2_AS_RF_SWITCH: true
|
||||||
spidev: spidev0.0
|
spidev: spidev0.0
|
||||||
# CS: 8
|
# CS: 8
|
||||||
|
|
||||||
|
|
||||||
### RAK13300in Slot 2 pins
|
|
||||||
# IRQ: 18 #IO6
|
|
||||||
# Reset: 24 # IO4
|
|
||||||
# Busy: 19 # IO5
|
|
||||||
# # Ant_sw: 23 # IO3
|
|
||||||
# spidev: spidev0.1
|
|
||||||
# # CS: 7
|
|
||||||
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
8
bin/config.d/lora-RAK6421-13300-slot2.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Lora:
|
||||||
|
### RAK13300in Slot 2 pins
|
||||||
|
IRQ: 18 #IO6
|
||||||
|
Reset: 24 # IO4
|
||||||
|
Busy: 19 # IO5
|
||||||
|
# Ant_sw: 23 # IO3
|
||||||
|
spidev: spidev0.1
|
||||||
|
# CS: 7
|
||||||
52
bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml
Normal file
52
bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
|
||||||
|
# http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-Zero-3.html
|
||||||
|
#
|
||||||
|
# See Orange Pi Zero3 manual, chapter 3.16, page 124 for 26-pin header pinout
|
||||||
|
#
|
||||||
|
# Pin Connection
|
||||||
|
# Waveshare Orange Pi Zero3
|
||||||
|
# 36 3.3V 17
|
||||||
|
# 15 MOSI 19
|
||||||
|
# 16 MISO 21
|
||||||
|
# 14 CLK 23
|
||||||
|
# 38 GND 25
|
||||||
|
# 4 BUSY 18
|
||||||
|
# 20 RESET 22
|
||||||
|
# 5 CS 24
|
||||||
|
# 26 DIO1/IRQ 26
|
||||||
|
|
||||||
|
Lora:
|
||||||
|
Module: sx1262 # Waveshare Raspberry Pico Lora module
|
||||||
|
DIO2_AS_RF_SWITCH: true
|
||||||
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
# Specify either the spidev1_1 or the CS below, not both!
|
||||||
|
# On DietPi Linux, when using the user overlay dietpi-spi1_1.dtbo, CS will be configured with spidev1.1
|
||||||
|
spidev: spidev1.1 # See Orange Pi Zero3 manual, chapter 3.18.3, page 130
|
||||||
|
# CS: # CS PIN_24 -> chip 1, line 233
|
||||||
|
# pin: 24
|
||||||
|
# gpiochip: 1
|
||||||
|
# line: 233
|
||||||
|
SCK: # SCK PIN_23 -> chip 1, line 230
|
||||||
|
pin: 23
|
||||||
|
gpiochip: 1
|
||||||
|
line: 230
|
||||||
|
Busy: # BUSY PIN_18 -> chip 1, line 78
|
||||||
|
pin: 18
|
||||||
|
gpiochip: 1
|
||||||
|
line: 78
|
||||||
|
MOSI: # MOSI PIN_19 -> chip 1, line 231
|
||||||
|
pin: 19
|
||||||
|
gpiochip: 1
|
||||||
|
line: 231
|
||||||
|
MISO: # MISO PIN_21 -> chip 1, line 232
|
||||||
|
pin: 21
|
||||||
|
gpiochip: 1
|
||||||
|
line: 232
|
||||||
|
Reset: # NRST PIN_22 -> chip 1, line 71
|
||||||
|
pin: 22
|
||||||
|
gpiochip: 1
|
||||||
|
line: 71
|
||||||
|
IRQ: # DIO1 PIN_26 -> chip 1, line 74
|
||||||
|
pin: 26
|
||||||
|
gpiochip: 1
|
||||||
|
line: 74
|
||||||
@@ -5,7 +5,6 @@ TITLE Meshtastic device-install
|
|||||||
SET "SCRIPT_NAME=%~nx0"
|
SET "SCRIPT_NAME=%~nx0"
|
||||||
SET "DEBUG=0"
|
SET "DEBUG=0"
|
||||||
SET "PYTHON="
|
SET "PYTHON="
|
||||||
SET "WEB_APP=0"
|
|
||||||
SET "TFT_BUILD=0"
|
SET "TFT_BUILD=0"
|
||||||
SET "BIGDB8=0"
|
SET "BIGDB8=0"
|
||||||
SET "BIGDB16=0"
|
SET "BIGDB16=0"
|
||||||
@@ -25,7 +24,7 @@ GOTO getopts
|
|||||||
:help
|
:help
|
||||||
ECHO Flash image file to device, but first erasing and writing system information.
|
ECHO Flash image file to device, but first erasing and writing system information.
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) [--1200bps-reset]
|
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset]
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Options:
|
ECHO Options:
|
||||||
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
|
ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
|
||||||
@@ -35,13 +34,12 @@ ECHO If not set, ESPTOOL iterates all ports (Dangerous).
|
|||||||
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python)
|
||||||
ECHO If supplied the script will use python.
|
ECHO If supplied the script will use python.
|
||||||
ECHO If not supplied the script will try to find esptool in Path.
|
ECHO If not supplied the script will try to find esptool in Path.
|
||||||
ECHO --web Enable WebUI. (default: false)
|
|
||||||
ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset)
|
ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset)
|
||||||
ECHO Some hardware requires this twice.
|
ECHO Some hardware requires this twice.
|
||||||
ECHO.
|
ECHO.
|
||||||
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
|
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
|
||||||
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
|
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
|
||||||
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web
|
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11
|
||||||
GOTO eof
|
GOTO eof
|
||||||
|
|
||||||
:version
|
:version
|
||||||
@@ -61,7 +59,6 @@ IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT
|
|||||||
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||||
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT
|
||||||
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT
|
||||||
IF /I "%~1"=="--web" SET "WEB_APP=1"
|
|
||||||
IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1"
|
IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1"
|
||||||
SHIFT
|
SHIFT
|
||||||
GOTO getopts
|
GOTO getopts
|
||||||
@@ -153,9 +150,6 @@ IF %BPS_RESET% EQU 1 (
|
|||||||
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
|
@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
|
||||||
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
|
IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
|
||||||
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
|
||||||
IF %WEB_APP% EQU 1 (
|
|
||||||
CALL :LOG_MESSAGE ERROR "Cannot enable WebUI (--web) and MUI." & GOTO eof
|
|
||||||
)
|
|
||||||
SET "TFT_BUILD=1"
|
SET "TFT_BUILD=1"
|
||||||
) ELSE (
|
) ELSE (
|
||||||
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
|
||||||
@@ -209,13 +203,8 @@ SET "OTA_FILENAME=bleota.bin"
|
|||||||
:end_loop_c3
|
:end_loop_c3
|
||||||
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
|
||||||
|
|
||||||
@REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-".
|
@REM Set SPIFFS filename with "littlefs-" prefix.
|
||||||
IF %WEB_APP% EQU 1 (
|
SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
|
||||||
CALL :LOG_MESSAGE INFO "WebUI selected."
|
|
||||||
SET "SPIFFS_FILENAME=littlefswebui-%BASENAME%"
|
|
||||||
) ELSE (
|
|
||||||
SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
|
|
||||||
)
|
|
||||||
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
|
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
|
||||||
|
|
||||||
@REM Default offsets.
|
@REM Default offsets.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
||||||
WEB_APP=false
|
|
||||||
BPS_RESET=false
|
BPS_RESET=false
|
||||||
TFT_BUILD=false
|
TFT_BUILD=false
|
||||||
MCU=""
|
MCU=""
|
||||||
@@ -76,14 +75,13 @@ set -e
|
|||||||
# Usage info
|
# Usage info
|
||||||
show_help() {
|
show_help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--web] [--1200bps-reset]
|
Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME] [--1200bps-reset]
|
||||||
Flash image file to device, but first erasing and writing system information.
|
Flash image file to device, but first erasing and writing system information.
|
||||||
|
|
||||||
-h Display this help and exit.
|
-h Display this help and exit.
|
||||||
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
|
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
|
||||||
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
|
||||||
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
|
-f FILENAME The firmware .bin file to flash. Custom to your device type and region.
|
||||||
--web Enable WebUI. (Default: false)
|
|
||||||
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
@@ -107,9 +105,6 @@ while [ $# -gt 0 ]; do
|
|||||||
FILENAME="$2"
|
FILENAME="$2"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--web)
|
|
||||||
WEB_APP=true
|
|
||||||
;;
|
|
||||||
--1200bps-reset)
|
--1200bps-reset)
|
||||||
BPS_RESET=true
|
BPS_RESET=true
|
||||||
;;
|
;;
|
||||||
@@ -140,20 +135,16 @@ if [[ "$FILENAME" != firmware-* ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if FILENAME contains "-tft-" and prevent web/mui comingling.
|
# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
|
||||||
if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then
|
if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then
|
||||||
TFT_BUILD=true
|
TFT_BUILD=true
|
||||||
if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then
|
|
||||||
echo "Cannot enable WebUI (--web) and MUI."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract BASENAME from %FILENAME% for later use.
|
# Extract BASENAME from %FILENAME% for later use.
|
||||||
BASENAME="${FILENAME/firmware-/}"
|
BASENAME="${FILENAME/firmware-/}"
|
||||||
|
|
||||||
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
||||||
# Default littlefs* offset (--web).
|
# Default littlefs* offset.
|
||||||
OFFSET=0x300000
|
OFFSET=0x300000
|
||||||
|
|
||||||
# Default OTA Offset
|
# Default OTA Offset
|
||||||
@@ -193,12 +184,8 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
|
|||||||
OTAFILE=bleota-s3.bin
|
OTAFILE=bleota-s3.bin
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-".
|
# Set SPIFFS filename with "littlefs-" prefix.
|
||||||
if [ "$WEB_APP" = true ]; then
|
SPIFFSFILE=littlefs-${BASENAME}
|
||||||
SPIFFSFILE=littlefswebui-${BASENAME}
|
|
||||||
else
|
|
||||||
SPIFFSFILE=littlefs-${BASENAME}
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$FILENAME" ]]; then
|
if [[ ! -f "$FILENAME" ]]; then
|
||||||
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
echo "Error: file ${FILENAME} wasn't found. Terminating."
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
||||||
CHANGE_MODE=false
|
CHANGE_MODE=false
|
||||||
@@ -30,6 +30,17 @@ Flash image file to device, leave existing system intact."
|
|||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check for --change-mode and remove it from arguments
|
||||||
|
NEW_ARGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$arg" = "--change-mode" ]; then
|
||||||
|
CHANGE_MODE=true
|
||||||
|
else
|
||||||
|
NEW_ARGS+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
set -- "${NEW_ARGS[@]}"
|
||||||
|
|
||||||
while getopts ":hp:P:f:" opt; do
|
while getopts ":hp:P:f:" opt; do
|
||||||
case "${opt}" in
|
case "${opt}" in
|
||||||
@@ -43,9 +54,6 @@ while getopts ":hp:P:f:" opt; do
|
|||||||
;;
|
;;
|
||||||
f) FILENAME=${OPTARG}
|
f) FILENAME=${OPTARG}
|
||||||
;;
|
;;
|
||||||
--change-mode)
|
|
||||||
CHANGE_MODE=true
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Invalid flag."
|
echo "Invalid flag."
|
||||||
show_help >&2
|
show_help >&2
|
||||||
@@ -55,7 +63,7 @@ while getopts ":hp:P:f:" opt; do
|
|||||||
done
|
done
|
||||||
shift "$((OPTIND-1))"
|
shift "$((OPTIND-1))"
|
||||||
|
|
||||||
if [[ $CHANGE_MODE == true ]]; then
|
if [ "$CHANGE_MODE" = true ]; then
|
||||||
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
$ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -2,50 +2,71 @@
|
|||||||
|
|
||||||
"""Generate the CI matrix."""
|
"""Generate the CI matrix."""
|
||||||
|
|
||||||
import configparser
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
rootdir = "variants/"
|
from platformio.project.config import ProjectConfig
|
||||||
|
|
||||||
options = sys.argv[1:]
|
options = sys.argv[1:]
|
||||||
|
|
||||||
outlist = []
|
outlist = []
|
||||||
|
|
||||||
if len(options) < 1:
|
if len(options) < 1:
|
||||||
print(json.dumps(outlist))
|
print(json.dumps(outlist))
|
||||||
exit()
|
exit(1)
|
||||||
|
|
||||||
for subdir, dirs, files in os.walk(rootdir):
|
cfg = ProjectConfig.get_instance()
|
||||||
for file in files:
|
pio_envs = cfg.envs()
|
||||||
if file == "platformio.ini":
|
|
||||||
config = configparser.ConfigParser()
|
# Gather all PlatformIO environments for filtering later
|
||||||
config.read(subdir + "/" + file)
|
all_envs = []
|
||||||
for c in config.sections():
|
for pio_env in pio_envs:
|
||||||
if c.startswith("env:"):
|
env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags')
|
||||||
section = config[c].name[4:]
|
env_platform = None
|
||||||
if "extends" in config[config[c].name]:
|
for flag in env_build_flags:
|
||||||
if options[0] + "_base" in config[config[c].name]["extends"]:
|
# Extract the platform from the build flags
|
||||||
if "board_level" in config[config[c].name]:
|
# Example flag: -I variants/esp32s3/heltec-v3
|
||||||
if (
|
match = re.search(r"-I\s?variants/([^/]+)", flag)
|
||||||
config[config[c].name]["board_level"] == "extra"
|
if match:
|
||||||
) & ("extra" in options):
|
env_platform = match.group(1)
|
||||||
outlist.append(section)
|
break
|
||||||
else:
|
# Intentionally fail if platform cannot be determined
|
||||||
outlist.append(section)
|
if not env_platform:
|
||||||
# Add the TFT variants if the base variant is selected
|
print(f"Error: Could not determine platform for environment '{pio_env}'")
|
||||||
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
exit(1)
|
||||||
outlist.append(section)
|
# Store env details as a dictionary, and add to 'all_envs' list
|
||||||
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
|
env = {
|
||||||
outlist.append(section)
|
'name': pio_env,
|
||||||
if "board_check" in config[config[c].name]:
|
'platform': env_platform,
|
||||||
if (config[config[c].name]["board_check"] == "true") & (
|
'board_level': cfg.get(f"env:{pio_env}", 'board_level', default=None),
|
||||||
"check" in options
|
'board_check': bool(cfg.get(f"env:{pio_env}", 'board_check', default=False))
|
||||||
):
|
}
|
||||||
outlist.append(section)
|
all_envs.append(env)
|
||||||
if ("quick" in options) & (len(outlist) > 3):
|
|
||||||
print(json.dumps(random.sample(outlist, 3)))
|
# Filter outputs based on options
|
||||||
|
# Check is mutually exclusive with other options (except 'pr')
|
||||||
|
if "check" in options:
|
||||||
|
for env in all_envs:
|
||||||
|
if env['board_check']:
|
||||||
|
if "pr" in options:
|
||||||
|
if env['board_level'] == 'pr':
|
||||||
|
outlist.append(env['name'])
|
||||||
|
else:
|
||||||
|
outlist.append(env['name'])
|
||||||
|
# Filter (non-check) builds by platform
|
||||||
else:
|
else:
|
||||||
print(json.dumps(outlist))
|
for env in all_envs:
|
||||||
|
if options[0] == env['platform']:
|
||||||
|
# Always include board_level = 'pr'
|
||||||
|
if env['board_level'] == 'pr':
|
||||||
|
outlist.append(env['name'])
|
||||||
|
# Include board_level = 'extra' when requested
|
||||||
|
elif "extra" in options and env['board_level'] == "extra":
|
||||||
|
outlist.append(env['name'])
|
||||||
|
# If no board level is specified, include in release builds (not PR)
|
||||||
|
elif "pr" not in options and not env['board_level']:
|
||||||
|
outlist.append(env['name'])
|
||||||
|
|
||||||
|
# Return as a JSON list
|
||||||
|
print(json.dumps(outlist))
|
||||||
|
|||||||
@@ -87,7 +87,22 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="2.7.1" date="2025-06-21">
|
<release version="2.7.6" date="2025-08-12">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.5" date="2025-08-09">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.4" date="2025-07-19">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.3" date="2025-07-10">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.2" date="2025-07-04">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.1" date="2025-06-27">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1</url>
|
||||||
</release>
|
</release>
|
||||||
<release version="2.7.0" date="2025-06-20">
|
<release version="2.7.0" date="2025-06-20">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||||
import sys
|
import sys
|
||||||
from os.path import join
|
from os.path import join
|
||||||
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -92,6 +93,17 @@ prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
|
|||||||
verObj = readProps(prefsLoc)
|
verObj = readProps(prefsLoc)
|
||||||
print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
|
print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
|
||||||
|
|
||||||
|
# get repository owner if git is installed
|
||||||
|
try:
|
||||||
|
r_owner = (
|
||||||
|
subprocess.check_output(["git", "config", "--get", "remote.origin.url"])
|
||||||
|
.decode("utf-8")
|
||||||
|
.strip().split("/")
|
||||||
|
)
|
||||||
|
repo_owner = r_owner[-2] + "/" + r_owner[-1].replace(".git", "")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
repo_owner = "unknown"
|
||||||
|
|
||||||
jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc"
|
jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc"
|
||||||
with open(jsonLoc) as f:
|
with open(jsonLoc) as f:
|
||||||
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
|
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
|
||||||
@@ -117,6 +129,7 @@ flags = [
|
|||||||
"-DAPP_VERSION=" + verObj["long"],
|
"-DAPP_VERSION=" + verObj["long"],
|
||||||
"-DAPP_VERSION_SHORT=" + verObj["short"],
|
"-DAPP_VERSION_SHORT=" + verObj["short"],
|
||||||
"-DAPP_ENV=" + env.get("PIOENV"),
|
"-DAPP_ENV=" + env.get("PIOENV"),
|
||||||
|
"-DAPP_REPO=" + repo_owner,
|
||||||
] + pref_flags
|
] + pref_flags
|
||||||
|
|
||||||
print ("Using flags:")
|
print ("Using flags:")
|
||||||
@@ -131,3 +144,33 @@ for lb in env.GetLibBuilders():
|
|||||||
if lb.name == "meshtastic-device-ui":
|
if lb.name == "meshtastic-device-ui":
|
||||||
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
lb.env.Append(CPPDEFINES=[("APP_VERSION", verObj["long"])])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Get the display resolution from macros
|
||||||
|
def get_display_resolution(build_flags):
|
||||||
|
# Check "DISPLAY_SIZE" to determine the screen resolution
|
||||||
|
for flag in build_flags:
|
||||||
|
if isinstance(flag, tuple) and flag[0] == "DISPLAY_SIZE":
|
||||||
|
screen_width, screen_height = map(int, flag[1].split("x"))
|
||||||
|
return screen_width, screen_height
|
||||||
|
print("No screen resolution defined in build_flags. Please define DISPLAY_SIZE.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def load_boot_logo(source, target, env):
|
||||||
|
build_flags = env.get("CPPDEFINES", [])
|
||||||
|
logo_w, logo_h = get_display_resolution(build_flags)
|
||||||
|
print(f"TFT build with {logo_w}x{logo_h} resolution detected")
|
||||||
|
|
||||||
|
# Load the boot logo from `branding/logo_<width>x<height>.png` if it exists
|
||||||
|
source_path = join(env["PROJECT_DIR"], "branding", f"logo_{logo_w}x{logo_h}.png")
|
||||||
|
dest_dir = join(env["PROJECT_DIR"], "data", "boot")
|
||||||
|
dest_path = join(dest_dir, "logo.png")
|
||||||
|
if env.File(source_path).exists():
|
||||||
|
print(f"Loading boot logo from {source_path}")
|
||||||
|
# Prepare the destination
|
||||||
|
env.Execute(f"mkdir -p {dest_dir} && rm -f {dest_path}")
|
||||||
|
# Copy the logo to the `data/boot` directory
|
||||||
|
env.Execute(f"cp {source_path} {dest_path}")
|
||||||
|
|
||||||
|
# Load the boot logo on TFT builds
|
||||||
|
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||||
|
env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.5.3
|
2.6.4
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"hwids": [
|
"hwids": [
|
||||||
["0x239A", "0x4405"],
|
["0x239A", "0x4405"],
|
||||||
["0x239A", "0x0029"],
|
["0x239A", "0x0029"],
|
||||||
["0x239A", "0x002A"]
|
["0x239A", "0x002A"],
|
||||||
|
["0x2886", "0x1667"]
|
||||||
],
|
],
|
||||||
"usb_product": "HT-n5262",
|
"usb_product": "HT-n5262",
|
||||||
"mcu": "nrf52840",
|
"mcu": "nrf52840",
|
||||||
|
|||||||
52
boards/meshtiny.json
Normal file
52
boards/meshtiny.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "nrf52840_s140_v6.ld"
|
||||||
|
},
|
||||||
|
"core": "nRF5",
|
||||||
|
"cpu": "cortex-m4",
|
||||||
|
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
|
||||||
|
"f_cpu": "64000000L",
|
||||||
|
"hwids": [
|
||||||
|
["0x239A", "0x8029"],
|
||||||
|
["0x239A", "0x0029"],
|
||||||
|
["0x239A", "0x002A"],
|
||||||
|
["0x239A", "0x802A"]
|
||||||
|
],
|
||||||
|
"usb_product": "MeshTiny",
|
||||||
|
"mcu": "nrf52840",
|
||||||
|
"variant": "meshtiny",
|
||||||
|
"bsp": {
|
||||||
|
"name": "adafruit"
|
||||||
|
},
|
||||||
|
"softdevice": {
|
||||||
|
"sd_flags": "-DS140",
|
||||||
|
"sd_name": "s140",
|
||||||
|
"sd_version": "6.1.1",
|
||||||
|
"sd_fwid": "0x00B6"
|
||||||
|
},
|
||||||
|
"bootloader": {
|
||||||
|
"settings_addr": "0xFF000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectivity": ["bluetooth"],
|
||||||
|
"debug": {
|
||||||
|
"jlink_device": "nRF52840_xxAA",
|
||||||
|
"svd_path": "nrf52840.svd",
|
||||||
|
"openocd_target": "nrf52840-mdk-rs"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "freertos"],
|
||||||
|
"name": "MeshTiny",
|
||||||
|
"upload": {
|
||||||
|
"maximum_ram_size": 248832,
|
||||||
|
"maximum_size": 815104,
|
||||||
|
"speed": 115200,
|
||||||
|
"protocol": "nrfutil",
|
||||||
|
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"wait_for_upload_port": true
|
||||||
|
},
|
||||||
|
"url": "https://github.com/meshtastic/firmware",
|
||||||
|
"vendor": "MTools Tec"
|
||||||
|
}
|
||||||
@@ -7,7 +7,10 @@
|
|||||||
"cpu": "cortex-m4",
|
"cpu": "cortex-m4",
|
||||||
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
|
"extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA",
|
||||||
"f_cpu": "64000000L",
|
"f_cpu": "64000000L",
|
||||||
"hwids": [["0x2886", "0x1668"]],
|
"hwids": [
|
||||||
|
["0x2886", "0x1668"],
|
||||||
|
["0x2886", "0x1667"]
|
||||||
|
],
|
||||||
"usb_product": "TRACKER L1",
|
"usb_product": "TRACKER L1",
|
||||||
"mcu": "nrf52840",
|
"mcu": "nrf52840",
|
||||||
"variant": "seeed_wio_tracker_L1",
|
"variant": "seeed_wio_tracker_L1",
|
||||||
|
|||||||
43
boards/t-deck-pro.json
Normal file
43
boards/t-deck-pro.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"memory_type": "qio_qspi",
|
||||||
|
"partitions": "default_16MB.csv"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"hwids": [["0x303A", "0x1001"]],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "esp32s3"
|
||||||
|
},
|
||||||
|
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||||
|
"debug": {
|
||||||
|
"default_tool": "esp-builtin",
|
||||||
|
"onboard_tools": ["esp-builtin"],
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "espidf"],
|
||||||
|
"name": "LilyGo T-Deck Pro S3 (16M Flash 8M QSPI PSRAM )",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"speed": 115200
|
||||||
|
},
|
||||||
|
"url": "https://lilygo.cc/products/t-deck-pro",
|
||||||
|
"vendor": "LilyGo"
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
["0x239A", "0x8029"],
|
["0x239A", "0x8029"],
|
||||||
["0x239A", "0x0029"],
|
["0x239A", "0x0029"],
|
||||||
["0x239A", "0x002A"],
|
["0x239A", "0x002A"],
|
||||||
["0x239A", "0x802A"]
|
["0x239A", "0x802A"],
|
||||||
|
["0x2886", "0x0057"]
|
||||||
],
|
],
|
||||||
"usb_product": "T1000-E-BOOT",
|
"usb_product": "T1000-E-BOOT",
|
||||||
"mcu": "nrf52840",
|
"mcu": "nrf52840",
|
||||||
|
|||||||
41
boards/wiscore_rak3312.json
Normal file
41
boards/wiscore_rak3312.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"memory_type": "qio_opi",
|
||||||
|
"partitions": "default_16MB.csv"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DRAK3312",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||||
|
"-DBOARD_HAS_PSRAM"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"hwids": [["0x303A", "0x1001"]],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "rak3312"
|
||||||
|
},
|
||||||
|
"connectivity": ["wifi", "bluetooth"],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "espidf"],
|
||||||
|
"name": "WisCore RAK3312 Board",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"wait_for_upload_port": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"url": "https://www.rakwireless.com/en-us",
|
||||||
|
"vendor": "rakwireless"
|
||||||
|
}
|
||||||
17
branding/README.md
Normal file
17
branding/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Meshtastic Branding / Whitelabeling
|
||||||
|
|
||||||
|
This directory is consumed during the creation of **event** firmware.
|
||||||
|
|
||||||
|
`bin/platformio-custom.py` determines the display resolution, and locates the corresponding `logo_<width>x<height>.png`.
|
||||||
|
|
||||||
|
Ex:
|
||||||
|
|
||||||
|
- `logo_800x480.png`
|
||||||
|
- `logo_480x480.png`
|
||||||
|
- `logo_480x320.png`
|
||||||
|
- `logo_320x480.png`
|
||||||
|
- `logo_320x240.png`
|
||||||
|
|
||||||
|
This file is copied to `data/boot/logo.png` before filesytem image compilation.
|
||||||
|
|
||||||
|
For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33).
|
||||||
19
debian/changelog
vendored
19
debian/changelog
vendored
@@ -1,4 +1,4 @@
|
|||||||
meshtasticd (2.7.1.0) UNRELEASED; urgency=medium
|
meshtasticd (2.7.6.0) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
[ Austin Lane ]
|
[ Austin Lane ]
|
||||||
* Initial packaging
|
* Initial packaging
|
||||||
@@ -25,4 +25,19 @@ meshtasticd (2.7.1.0) UNRELEASED; urgency=medium
|
|||||||
[ ]
|
[ ]
|
||||||
* GitHub Actions Automatic version bump
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
-- <github-actions[bot]@users.noreply.github.com> Sat, 21 Jun 2025 15:51:49 +0000
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ Ubuntu ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
-- <github-actions[bot]@users.noreply.github.com> Tue, 12 Aug 2025 23:48:48 +0000
|
||||||
|
|||||||
2
debian/meshtasticd.postinst
vendored
2
debian/meshtasticd.postinst
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# postinst script for meshtasticd
|
# postinst script for meshtasticd
|
||||||
#
|
#
|
||||||
# see: dh_installdeb(1)
|
# see: dh_installdeb(1)
|
||||||
|
|||||||
2
debian/meshtasticd.postrm
vendored
2
debian/meshtasticd.postrm
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# postrm script for meshtasticd
|
# postrm script for meshtasticd
|
||||||
#
|
#
|
||||||
# see: dh_installdeb(1)
|
# see: dh_installdeb(1)
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ default_envs = tbeam
|
|||||||
|
|
||||||
extra_configs =
|
extra_configs =
|
||||||
arch/*/*.ini
|
arch/*/*.ini
|
||||||
variants/*/platformio.ini
|
variants/*/*/platformio.ini
|
||||||
|
variants/*/diy/*/platformio.ini
|
||||||
src/graphics/niche/InkHUD/PlatformioConfig.ini
|
src/graphics/niche/InkHUD/PlatformioConfig.ini
|
||||||
|
|
||||||
description = Meshtastic
|
description = Meshtastic
|
||||||
@@ -49,19 +50,19 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
-DMESHTASTIC_EXCLUDE_DROPZONE=1
|
||||||
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
|
||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME
|
#-DBUILD_EPOCH=$UNIX_TIME
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip
|
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
|
||||||
# renovate: datasource=custom.pio depName=OneButton packageName=mathertel/library/OneButton
|
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||||
mathertel/OneButton@2.6.1
|
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||||
https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip
|
https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
|
||||||
@@ -101,21 +102,29 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
|
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
|
||||||
arcao/Syslog@2.0.0
|
arcao/Syslog@2.0.0
|
||||||
|
|
||||||
|
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
|
||||||
|
[nrf52_networking_base]
|
||||||
|
lib_deps =
|
||||||
|
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
|
||||||
|
thingsboard/TBPubSubClient@2.12.1
|
||||||
|
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
|
||||||
|
arduino-libraries/NTPClient@3.2.1
|
||||||
|
|
||||||
[radiolib_base]
|
[radiolib_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||||
jgromes/RadioLib@7.2.0
|
jgromes/RadioLib@7.2.1
|
||||||
|
|
||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/cdc6e5bdeedb8293d10e4a02be6ca64e95a7c515.zip
|
https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
|
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
|
||||||
adafruit/Adafruit BusIO@1.17.1
|
adafruit/Adafruit BusIO@1.17.2
|
||||||
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
|
||||||
adafruit/Adafruit Unified Sensor@1.1.15
|
adafruit/Adafruit Unified Sensor@1.1.15
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
|
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
|
||||||
@@ -129,7 +138,7 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
|
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
|
||||||
adafruit/Adafruit MCP9808 Library@2.0.2
|
adafruit/Adafruit MCP9808 Library@2.0.2
|
||||||
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
|
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
|
||||||
adafruit/Adafruit INA260 Library@1.5.2
|
adafruit/Adafruit INA260 Library@1.5.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
|
||||||
adafruit/Adafruit INA219@1.2.3
|
adafruit/Adafruit INA219@1.2.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
|
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
|
||||||
@@ -168,8 +177,8 @@ lib_deps =
|
|||||||
adafruit/Adafruit PCT2075@1.0.5
|
adafruit/Adafruit PCT2075@1.0.5
|
||||||
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
|
||||||
dfrobot/DFRobot_BMM150@1.0.0
|
dfrobot/DFRobot_BMM150@1.0.0
|
||||||
|
|
||||||
; (not included in native / portduino)
|
; (not included in native / portduino)
|
||||||
[environmental_extra]
|
[environmental_extra]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
# renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library
|
||||||
@@ -177,7 +186,7 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
|
||||||
adafruit/Adafruit MAX1704X@1.0.3
|
adafruit/Adafruit MAX1704X@1.0.3
|
||||||
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
|
||||||
adafruit/Adafruit SHTC3 Library@1.0.1
|
adafruit/Adafruit SHTC3 Library@1.0.2
|
||||||
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
|
||||||
adafruit/Adafruit LPS2X@2.0.6
|
adafruit/Adafruit LPS2X@2.0.6
|
||||||
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library
|
||||||
@@ -195,4 +204,8 @@ lib_deps =
|
|||||||
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
|
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
|
||||||
boschsensortec/BME68x Sensor Library@1.3.40408
|
boschsensortec/BME68x Sensor Library@1.3.40408
|
||||||
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
|
||||||
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
|
||||||
|
sensirion/Sensirion Core@0.7.1
|
||||||
|
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
|
||||||
|
sensirion/Sensirion I2C SCD4x@1.1.0
|
||||||
|
|||||||
Submodule protobufs updated: 386fa53c15...8985852d75
@@ -89,14 +89,22 @@ class BluetoothStatus : public Status
|
|||||||
case ConnectionState::CONNECTED:
|
case ConnectionState::CONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus CONNECTED");
|
LOG_DEBUG("BluetoothStatus CONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
|
#ifdef BLE_LED_INVERTED
|
||||||
|
digitalWrite(BLE_LED, LOW);
|
||||||
|
#else
|
||||||
digitalWrite(BLE_LED, HIGH);
|
digitalWrite(BLE_LED, HIGH);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ConnectionState::DISCONNECTED:
|
case ConnectionState::DISCONNECTED:
|
||||||
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
LOG_DEBUG("BluetoothStatus DISCONNECTED");
|
||||||
#ifdef BLE_LED
|
#ifdef BLE_LED
|
||||||
|
#ifdef BLE_LED_INVERTED
|
||||||
|
digitalWrite(BLE_LED, HIGH);
|
||||||
|
#else
|
||||||
digitalWrite(BLE_LED, LOW);
|
digitalWrite(BLE_LED, LOW);
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
228
src/Power.cpp
228
src/Power.cpp
@@ -20,6 +20,11 @@
|
|||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
#include "api/WiFiServerAPI.h"
|
||||||
|
#include "input/LinuxInputImpl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
// Working USB detection for powered/charging states on the RAK platform
|
// Working USB detection for powered/charging states on the RAK platform
|
||||||
#ifdef NRF_APM
|
#ifdef NRF_APM
|
||||||
#include "nrfx_power.h"
|
#include "nrfx_power.h"
|
||||||
@@ -103,7 +108,7 @@ NullSensor ina3221Sensor;
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_STM32WL)
|
#if !MESHTASTIC_EXCLUDE_I2C
|
||||||
#include "modules/Telemetry/Sensor/MAX17048Sensor.h"
|
#include "modules/Telemetry/Sensor/MAX17048Sensor.h"
|
||||||
#include <utility>
|
#include <utility>
|
||||||
extern std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1];
|
extern std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1];
|
||||||
@@ -120,6 +125,15 @@ NullSensor max17048Sensor;
|
|||||||
RAK9154Sensor rak9154Sensor;
|
RAK9154Sensor rak9154Sensor;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_PPM
|
||||||
|
// note: XPOWERS_CHIP_XXX must be defined in variant.h
|
||||||
|
#include <XPowersLib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAS_BQ27220
|
||||||
|
#include "bq27220.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_PMU
|
#ifdef HAS_PMU
|
||||||
XPowersLibInterface *PMU = NULL;
|
XPowersLibInterface *PMU = NULL;
|
||||||
#else
|
#else
|
||||||
@@ -278,7 +292,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !defined(ARCH_STM32WL) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
#if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
if (hasINA()) {
|
if (hasINA()) {
|
||||||
return getINAVoltage();
|
return getINAVoltage();
|
||||||
}
|
}
|
||||||
@@ -456,8 +470,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
#ifdef EXT_CHRG_DETECT
|
#ifdef EXT_CHRG_DETECT
|
||||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||||
#else
|
#else
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) && \
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||||||
!defined(DISABLE_INA_CHARGING_DETECTION)
|
|
||||||
if (hasINA()) {
|
if (hasINA()) {
|
||||||
// get current flow from INA sensor - negative value means power flowing into the battery
|
// get current flow from INA sensor - negative value means power flowing into the battery
|
||||||
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
// default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD
|
||||||
@@ -503,7 +516,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL)
|
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||||
uint16_t getINAVoltage()
|
uint16_t getINAVoltage()
|
||||||
{
|
{
|
||||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
|
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
|
||||||
@@ -666,6 +679,8 @@ bool Power::setup()
|
|||||||
found = true;
|
found = true;
|
||||||
} else if (lipoInit()) {
|
} else if (lipoInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
|
} else if (lipoChargerInit()) {
|
||||||
|
found = true;
|
||||||
} else if (analogInit()) {
|
} else if (analogInit()) {
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
@@ -680,9 +695,61 @@ bool Power::setup()
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Power::powerCommandsCheck()
|
||||||
|
{
|
||||||
|
if (rebootAtMsec && millis() > rebootAtMsec) {
|
||||||
|
LOG_INFO("Rebooting");
|
||||||
|
reboot();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shutdownAtMsec && millis() > shutdownAtMsec) {
|
||||||
|
shutdownAtMsec = 0;
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Power::reboot()
|
||||||
|
{
|
||||||
|
notifyReboot.notifyObservers(NULL);
|
||||||
|
#if defined(ARCH_ESP32)
|
||||||
|
ESP.restart();
|
||||||
|
#elif defined(ARCH_NRF52)
|
||||||
|
NVIC_SystemReset();
|
||||||
|
#elif defined(ARCH_RP2040)
|
||||||
|
rp2040.reboot();
|
||||||
|
#elif defined(ARCH_PORTDUINO)
|
||||||
|
deInitApiServer();
|
||||||
|
if (aLinuxInputImpl)
|
||||||
|
aLinuxInputImpl->deInit();
|
||||||
|
SPI.end();
|
||||||
|
Wire.end();
|
||||||
|
Serial1.end();
|
||||||
|
if (screen) {
|
||||||
|
delete screen;
|
||||||
|
screen = nullptr;
|
||||||
|
}
|
||||||
|
LOG_DEBUG("final reboot!");
|
||||||
|
::reboot();
|
||||||
|
#elif defined(ARCH_STM32WL)
|
||||||
|
HAL_NVIC_SystemReset();
|
||||||
|
#else
|
||||||
|
rebootAtMsec = -1;
|
||||||
|
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Power::shutdown()
|
void Power::shutdown()
|
||||||
{
|
{
|
||||||
LOG_INFO("Shutting down");
|
|
||||||
|
#if HAS_SCREEN
|
||||||
|
if (screen) {
|
||||||
|
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if !defined(ARCH_STM32WL)
|
||||||
|
playShutdownMelody();
|
||||||
|
#endif
|
||||||
|
nodeDB->saveToDisk();
|
||||||
|
|
||||||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
|
||||||
#ifdef PIN_LED1
|
#ifdef PIN_LED1
|
||||||
@@ -694,7 +761,11 @@ void Power::shutdown()
|
|||||||
#ifdef PIN_LED3
|
#ifdef PIN_LED3
|
||||||
ledOff(PIN_LED3);
|
ledOff(PIN_LED3);
|
||||||
#endif
|
#endif
|
||||||
doDeepSleep(DELAY_FOREVER, false, false);
|
doDeepSleep(DELAY_FOREVER, false, true);
|
||||||
|
#elif defined(ARCH_PORTDUINO)
|
||||||
|
exit(EXIT_SUCCESS);
|
||||||
|
#else
|
||||||
|
LOG_WARN("FIXME implement shutdown for this platform");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1161,7 +1232,7 @@ bool Power::axpChipInit()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL)
|
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||||||
@@ -1238,3 +1309,144 @@ bool Power::lipoInit()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(HAS_PPM) && HAS_PPM
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter class for BQ25896/BQ27220 Lipo battery charger.
|
||||||
|
*/
|
||||||
|
class LipoCharger : public HasBatteryLevel
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
XPowersPPM *ppm = nullptr;
|
||||||
|
BQ27220 *bq = nullptr;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Init the I2C BQ25896 Lipo battery charger
|
||||||
|
*/
|
||||||
|
bool runOnce()
|
||||||
|
{
|
||||||
|
if (ppm == nullptr) {
|
||||||
|
ppm = new XPowersPPM;
|
||||||
|
bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||||||
|
if (result) {
|
||||||
|
LOG_INFO("PPM BQ25896 init succeeded");
|
||||||
|
// Set the minimum operating voltage. Below this voltage, the PPM will protect
|
||||||
|
// ppm->setSysPowerDownVoltage(3100);
|
||||||
|
|
||||||
|
// Set input current limit, default is 500mA
|
||||||
|
// ppm->setInputCurrentLimit(800);
|
||||||
|
|
||||||
|
// Disable current limit pin
|
||||||
|
// ppm->disableCurrentLimitPin();
|
||||||
|
|
||||||
|
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
|
||||||
|
ppm->setChargeTargetVoltage(4288);
|
||||||
|
|
||||||
|
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
|
||||||
|
// ppm->setPrechargeCurr(64);
|
||||||
|
|
||||||
|
// The premise is that limit pin is disabled, or it will
|
||||||
|
// only follow the maximum charging current set by limit pin.
|
||||||
|
// Set the charging current , Range:0~5056mA ,step:64mA
|
||||||
|
ppm->setChargerConstantCurr(1024);
|
||||||
|
|
||||||
|
// To obtain voltage data, the ADC must be enabled first
|
||||||
|
ppm->enableMeasure();
|
||||||
|
|
||||||
|
// Turn on charging function
|
||||||
|
// If there is no battery connected, do not turn on the charging function
|
||||||
|
ppm->enableCharge();
|
||||||
|
} else {
|
||||||
|
LOG_WARN("PPM BQ25896 init failed");
|
||||||
|
delete ppm;
|
||||||
|
ppm = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bq == nullptr) {
|
||||||
|
bq = new BQ27220;
|
||||||
|
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
|
||||||
|
|
||||||
|
bool result = bq->init();
|
||||||
|
if (result) {
|
||||||
|
LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
|
||||||
|
LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
|
||||||
|
LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG_WARN("BQ27220 init failed");
|
||||||
|
delete bq;
|
||||||
|
bq = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||||
|
*/
|
||||||
|
virtual int getBatteryPercent() override
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||||||
|
*/
|
||||||
|
virtual uint16_t getBattVoltage() override { return bq->getVoltage(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return true if there is a battery installed in this unit
|
||||||
|
*/
|
||||||
|
virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return true if there is an external power source detected
|
||||||
|
*/
|
||||||
|
virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return true if the battery is currently charging
|
||||||
|
*/
|
||||||
|
virtual bool isCharging() override
|
||||||
|
{
|
||||||
|
bool isCharging = ppm->isCharging();
|
||||||
|
if (isCharging) {
|
||||||
|
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
|
||||||
|
} else {
|
||||||
|
if (!ppm->isVbusIn()) {
|
||||||
|
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isCharging;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
LipoCharger lipoCharger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the Lipo battery charger
|
||||||
|
*/
|
||||||
|
bool Power::lipoChargerInit()
|
||||||
|
{
|
||||||
|
bool result = lipoCharger.runOnce();
|
||||||
|
LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
||||||
|
if (!result)
|
||||||
|
return false;
|
||||||
|
batteryLevel = &lipoCharger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
/**
|
||||||
|
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||||
|
*/
|
||||||
|
bool Power::lipoChargerInit()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ extern Power *power;
|
|||||||
static void shutdownEnter()
|
static void shutdownEnter()
|
||||||
{
|
{
|
||||||
LOG_DEBUG("State: SHUTDOWN");
|
LOG_DEBUG("State: SHUTDOWN");
|
||||||
power->shutdown();
|
shutdownAtMsec = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
|
|||||||
@@ -28,11 +28,14 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
case INPUT_BROKER_USER_PRESS:
|
case INPUT_BROKER_USER_PRESS:
|
||||||
case INPUT_BROKER_ALT_PRESS:
|
case INPUT_BROKER_ALT_PRESS:
|
||||||
case INPUT_BROKER_SELECT:
|
case INPUT_BROKER_SELECT:
|
||||||
|
case INPUT_BROKER_SELECT_LONG:
|
||||||
playBeep(); // Confirmation feedback
|
playBeep(); // Confirmation feedback
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case INPUT_BROKER_UP:
|
case INPUT_BROKER_UP:
|
||||||
|
case INPUT_BROKER_UP_LONG:
|
||||||
case INPUT_BROKER_DOWN:
|
case INPUT_BROKER_DOWN:
|
||||||
|
case INPUT_BROKER_DOWN_LONG:
|
||||||
case INPUT_BROKER_LEFT:
|
case INPUT_BROKER_LEFT:
|
||||||
case INPUT_BROKER_RIGHT:
|
case INPUT_BROKER_RIGHT:
|
||||||
playChirp(); // Navigation feedback
|
playChirp(); // Navigation feedback
|
||||||
@@ -47,10 +50,6 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
playComboTune(); // Ping sent feedback
|
playComboTune(); // Ping sent feedback
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case INPUT_BROKER_SHUTDOWN:
|
|
||||||
playShutdownMelody(); // Shutdown feedback
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// For other events, check if it's a printable character
|
// For other events, check if it's a printable character
|
||||||
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
||||||
@@ -69,10 +68,7 @@ int32_t BuzzerFeedbackThread::runOnce()
|
|||||||
// This thread is primarily event-driven, but we can use runOnce
|
// This thread is primarily event-driven, but we can use runOnce
|
||||||
// for any periodic tasks if needed in the future
|
// for any periodic tasks if needed in the future
|
||||||
|
|
||||||
if (needsUpdate) {
|
needsUpdate = false;
|
||||||
needsUpdate = false;
|
|
||||||
// Could add any periodic processing here
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run every 100ms when active, less frequently when idle
|
// Run every 100ms when active, less frequently when idle
|
||||||
return needsUpdate ? 100 : 1000;
|
return needsUpdate ? 100 : 1000;
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ bool playNextLeadUpNote()
|
|||||||
playTones(¬e, 1); // Play single note using existing playTones function
|
playTones(¬e, 1); // Play single note using existing playTones function
|
||||||
|
|
||||||
leadUpNoteIndex++;
|
leadUpNoteIndex++;
|
||||||
|
|
||||||
|
if (leadUpNoteIndex >= leadUpNotesCount) {
|
||||||
|
return false; // this was the final note
|
||||||
|
}
|
||||||
return true; // Note was played (playTones handles buzzer availability internally)
|
return true; // Note was played (playTones handles buzzer availability internally)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ enum class Cmd {
|
|||||||
START_FIRMWARE_UPDATE_SCREEN,
|
START_FIRMWARE_UPDATE_SCREEN,
|
||||||
STOP_BOOT_SCREEN,
|
STOP_BOOT_SCREEN,
|
||||||
SHOW_PREV_FRAME,
|
SHOW_PREV_FRAME,
|
||||||
SHOW_NEXT_FRAME
|
SHOW_NEXT_FRAME,
|
||||||
|
NOOP
|
||||||
};
|
};
|
||||||
@@ -150,11 +150,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// Define if screen should be mirrored left to right
|
// Define if screen should be mirrored left to right
|
||||||
// #define SCREEN_MIRROR
|
// #define SCREEN_MIRROR
|
||||||
|
|
||||||
// I2C Keyboards (M5Stack, RAK14004, T-Deck)
|
// I2C Keyboards (M5Stack, RAK14004, T-Deck, T-Deck Pro, T-Lora Pager, CardKB, BBQ10, MPR121, TCA8418)
|
||||||
#define CARDKB_ADDR 0x5F
|
#define CARDKB_ADDR 0x5F
|
||||||
#define TDECK_KB_ADDR 0x55
|
#define TDECK_KB_ADDR 0x55
|
||||||
#define BBQ10_KB_ADDR 0x1F
|
#define BBQ10_KB_ADDR 0x1F
|
||||||
#define MPR121_KB_ADDR 0x5A
|
#define MPR121_KB_ADDR 0x5A
|
||||||
|
#define TCA8418_KB_ADDR 0x34
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// SENSOR
|
// SENSOR
|
||||||
@@ -189,11 +190,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define DFROBOT_RAIN_ADDR 0x1d
|
#define DFROBOT_RAIN_ADDR 0x1d
|
||||||
#define NAU7802_ADDR 0x2A
|
#define NAU7802_ADDR 0x2A
|
||||||
#define MAX30102_ADDR 0x57
|
#define MAX30102_ADDR 0x57
|
||||||
|
#define SCD4X_ADDR 0x62
|
||||||
#define MLX90614_ADDR_DEF 0x5A
|
#define MLX90614_ADDR_DEF 0x5A
|
||||||
#define CGRADSENS_ADDR 0x66
|
#define CGRADSENS_ADDR 0x66
|
||||||
#define LTR390UV_ADDR 0x53
|
#define LTR390UV_ADDR 0x53
|
||||||
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
|
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418_KB
|
||||||
#define PCT2075_ADDR 0x37
|
#define PCT2075_ADDR 0x37
|
||||||
|
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
|
||||||
|
#define BQ25896_ADDR 0x6B
|
||||||
|
#define LTR553ALS_ADDR 0x23
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// ACCELEROMETER
|
// ACCELEROMETER
|
||||||
@@ -207,6 +212,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define BMX160_ADDR 0x69
|
#define BMX160_ADDR 0x69
|
||||||
#define ICM20948_ADDR 0x69
|
#define ICM20948_ADDR 0x69
|
||||||
#define ICM20948_ADDR_ALT 0x68
|
#define ICM20948_ADDR_ALT 0x68
|
||||||
|
#define BHI260AP_ADDR 0x28
|
||||||
#define BMM150_ADDR 0x13
|
#define BMM150_ADDR 0x13
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -229,6 +235,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// Touchscreen
|
// Touchscreen
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
#define FT6336U_ADDR 0x48
|
#define FT6336U_ADDR 0x48
|
||||||
|
#define CST328_ADDR 0x1A
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
|
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
|||||||
return firstOfOrNONE(9, types);
|
return firstOfOrNONE(9, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||||
|
{
|
||||||
|
ScanI2C::DeviceType types[] = {PMSA0031, SCD4X};
|
||||||
|
return firstOfOrNONE(2, types);
|
||||||
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
ScanI2C::FoundDevice ScanI2C::firstRGBLED() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {NCP5623, LP5562};
|
ScanI2C::DeviceType types[] = {NCP5623, LP5562};
|
||||||
@@ -80,4 +86,4 @@ bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) cons
|
|||||||
|| (port != NO_I2C && other.port != NO_I2C && (address < other.address));
|
|| (port != NO_I2C && other.port != NO_I2C && (address < other.address));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {}
|
ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ScanI2C
|
|||||||
FT6336U,
|
FT6336U,
|
||||||
STK8BAXX,
|
STK8BAXX,
|
||||||
ICM20948,
|
ICM20948,
|
||||||
|
SCD4X,
|
||||||
MAX30102,
|
MAX30102,
|
||||||
TPS65233,
|
TPS65233,
|
||||||
MPR121KB,
|
MPR121KB,
|
||||||
@@ -73,7 +74,12 @@ class ScanI2C
|
|||||||
RAK12035,
|
RAK12035,
|
||||||
TCA8418KB,
|
TCA8418KB,
|
||||||
PCT2075,
|
PCT2075,
|
||||||
BMM150,
|
CST328,
|
||||||
|
BQ25896,
|
||||||
|
BQ27220,
|
||||||
|
LTR553ALS,
|
||||||
|
BHI260AP,
|
||||||
|
BMM150
|
||||||
} DeviceType;
|
} DeviceType;
|
||||||
|
|
||||||
// typedef uint8_t DeviceAddress;
|
// typedef uint8_t DeviceAddress;
|
||||||
@@ -126,6 +132,8 @@ class ScanI2C
|
|||||||
|
|
||||||
FoundDevice firstAccelerometer() const;
|
FoundDevice firstAccelerometer() const;
|
||||||
|
|
||||||
|
FoundDevice firstAQI() const;
|
||||||
|
|
||||||
FoundDevice firstRGBLED() const;
|
FoundDevice firstRGBLED() const;
|
||||||
|
|
||||||
virtual FoundDevice find(DeviceType) const;
|
virtual FoundDevice find(DeviceType) const;
|
||||||
|
|||||||
@@ -184,8 +184,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
type = RTC_RV3028;
|
type = RTC_RV3028;
|
||||||
logFoundDevice("RV3028", (uint8_t)addr.address);
|
logFoundDevice("RV3028", (uint8_t)addr.address);
|
||||||
rtc.initI2C(*i2cBus);
|
rtc.initI2C(*i2cBus);
|
||||||
rtc.writeToRegister(0x35, 0x07); // no Clkout
|
// Update RTC EEPROM settings, if necessary
|
||||||
rtc.writeToRegister(0x37, 0xB4);
|
if (rtc.readEEPROMRegister(0x35) != 0x07) {
|
||||||
|
rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout
|
||||||
|
}
|
||||||
|
if (rtc.readEEPROMRegister(0x37) != 0xB4) {
|
||||||
|
rtc.writeEEPROMRegister(0x37, 0xB4);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -206,7 +211,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard", (uint8_t)addr.address);
|
case TDECK_KB_ADDR:
|
||||||
|
// Do we have the T-Deck keyboard or the T-Deck Pro battery sensor?
|
||||||
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
|
||||||
|
if (registerValue != 0) {
|
||||||
|
logFoundDevice("BQ27220", (uint8_t)addr.address);
|
||||||
|
type = BQ27220;
|
||||||
|
} else {
|
||||||
|
logFoundDevice("TDECKKB", (uint8_t)addr.address);
|
||||||
|
type = TDECKKB;
|
||||||
|
}
|
||||||
|
break;
|
||||||
SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address);
|
||||||
|
|
||||||
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
|
||||||
@@ -396,6 +411,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
logFoundDevice("BQ24295", (uint8_t)addr.address);
|
logFoundDevice("BQ24295", (uint8_t)addr.address);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID
|
||||||
|
if ((registerValue & 0b00000011) == 0b00000010) {
|
||||||
|
type = BQ25896;
|
||||||
|
logFoundDevice("BQ25896", (uint8_t)addr.address);
|
||||||
|
break;
|
||||||
|
}
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID
|
||||||
if (registerValue == 0x6A) {
|
if (registerValue == 0x6A) {
|
||||||
type = LSM6DS3;
|
type = LSM6DS3;
|
||||||
@@ -447,6 +468,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
|
||||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (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(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);
|
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
|
||||||
#ifdef HAS_TPS65233
|
#ifdef HAS_TPS65233
|
||||||
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
|
||||||
@@ -556,4 +581,4 @@ void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address)
|
|||||||
{
|
{
|
||||||
LOG_INFO("%s found at address 0x%x", device, address);
|
LOG_INFO("%s found at address 0x%x", device, address);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ template <typename T, std::size_t N> std::size_t array_count(const T (&)[N])
|
|||||||
return N;
|
return N;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
||||||
#if defined(RAK2560)
|
#if defined(GPS_SERIAL_PORT)
|
||||||
HardwareSerial *GPS::_serial_gps = &Serial2;
|
HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT;
|
||||||
#else
|
#else
|
||||||
HardwareSerial *GPS::_serial_gps = &Serial1;
|
HardwareSerial *GPS::_serial_gps = &Serial1;
|
||||||
#endif
|
#endif
|
||||||
@@ -643,8 +643,16 @@ bool GPS::setup()
|
|||||||
delay(250);
|
delay(250);
|
||||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
||||||
|
|
||||||
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
||||||
|
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
|
||||||
|
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
||||||
|
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||||
|
// 1 0 1 0 0 1
|
||||||
|
} else {
|
||||||
|
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
|
||||||
|
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||||
|
// 1 1 1 1 0 0
|
||||||
|
}
|
||||||
// Configure NMEA (sentences will output once per fix)
|
// Configure NMEA (sentences will output once per fix)
|
||||||
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
||||||
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
||||||
@@ -1503,7 +1511,7 @@ bool GPS::lookForTime()
|
|||||||
|
|
||||||
#ifdef GNSS_AIROHA
|
#ifdef GNSS_AIROHA
|
||||||
uint8_t fix = reader.fixQuality();
|
uint8_t fix = reader.fixQuality();
|
||||||
if (fix > 0) {
|
if (fix >= 1 && fix <= 5) {
|
||||||
if (lastFixStartMsec > 0) {
|
if (lastFixStartMsec > 0) {
|
||||||
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1536,7 +1544,10 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
|
|||||||
if (t.tm_mon > -1) {
|
if (t.tm_mon > -1) {
|
||||||
LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
|
LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
|
||||||
t.tm_sec, ti.age());
|
t.tm_sec, ti.age());
|
||||||
perhapsSetRTC(RTCQualityGPS, t);
|
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) {
|
||||||
|
// Clear the GPS buffer if we got an invalid time
|
||||||
|
clearBuffer();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else
|
} else
|
||||||
return false;
|
return false;
|
||||||
@@ -1555,7 +1566,7 @@ bool GPS::lookForLocation()
|
|||||||
#ifdef GNSS_AIROHA
|
#ifdef GNSS_AIROHA
|
||||||
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
|
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
|
||||||
uint8_t fix = reader.fixQuality();
|
uint8_t fix = reader.fixQuality();
|
||||||
if (fix > 0) {
|
if (fix >= 1 && fix <= 5) {
|
||||||
if (lastFixStartMsec > 0) {
|
if (lastFixStartMsec > 0) {
|
||||||
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ void readFromRTC()
|
|||||||
*
|
*
|
||||||
* If we haven't yet set our RTC this boot, set it from a GPS derived time
|
* If we haven't yet set our RTC this boot, set it from a GPS derived time
|
||||||
*/
|
*/
|
||||||
bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
|
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
|
||||||
{
|
{
|
||||||
static uint32_t lastSetMsec = 0;
|
static uint32_t lastSetMsec = 0;
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
@@ -113,7 +113,7 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
|
|||||||
#ifdef BUILD_EPOCH
|
#ifdef BUILD_EPOCH
|
||||||
if (tv->tv_sec < BUILD_EPOCH) {
|
if (tv->tv_sec < BUILD_EPOCH) {
|
||||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||||
return false;
|
return RTCSetResultInvalidTime;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -184,9 +184,9 @@ bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
|
|||||||
readFromRTC();
|
readFromRTC();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return true;
|
return RTCSetResultSuccess;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return RTCSetResultNotSet; // RTC was already set with a higher quality time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ const char *RtcName(RTCQuality quality)
|
|||||||
* @param t The time to potentially set the RTC to.
|
* @param t The time to potentially set the RTC to.
|
||||||
* @return True if the RTC was set to the provided time, false otherwise.
|
* @return True if the RTC was set to the provided time, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool perhapsSetRTC(RTCQuality q, struct tm &t)
|
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||||
{
|
{
|
||||||
/* Convert to unix time
|
/* Convert to unix time
|
||||||
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
|
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
|
||||||
@@ -226,12 +226,19 @@ bool perhapsSetRTC(RTCQuality q, struct tm &t)
|
|||||||
time_t res = gm_mktime(&t);
|
time_t res = gm_mktime(&t);
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
tv.tv_sec = res;
|
tv.tv_sec = res;
|
||||||
tv.tv_usec = 0; // time.centisecond() * (10 / 1000);
|
tv.tv_usec = 0; // time.centisecond() * (10 / 1000);
|
||||||
|
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||||
|
#ifdef BUILD_EPOCH
|
||||||
|
if (tv.tv_sec < BUILD_EPOCH) {
|
||||||
|
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||||
|
return RTCSetResultInvalidTime;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
|
// LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
|
||||||
if (t.tm_year < 0 || t.tm_year >= 300) {
|
if (t.tm_year < 0 || t.tm_year >= 300) {
|
||||||
// LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
|
// LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
|
||||||
return false;
|
return RTCSetResultInvalidTime;
|
||||||
} else {
|
} else {
|
||||||
return perhapsSetRTC(q, &tv);
|
return perhapsSetRTC(q, &tv);
|
||||||
}
|
}
|
||||||
@@ -244,11 +251,15 @@ bool perhapsSetRTC(RTCQuality q, struct tm &t)
|
|||||||
*/
|
*/
|
||||||
int32_t getTZOffset()
|
int32_t getTZOffset()
|
||||||
{
|
{
|
||||||
|
#if MESHTASTIC_EXCLUDE_TZ
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
time_t now = getTime(false);
|
time_t now = getTime(false);
|
||||||
struct tm *gmt;
|
struct tm *gmt;
|
||||||
gmt = gmtime(&now);
|
gmt = gmtime(&now);
|
||||||
gmt->tm_isdst = -1;
|
gmt->tm_isdst = -1;
|
||||||
return (int32_t)difftime(now, mktime(gmt));
|
return (int32_t)difftime(now, mktime(gmt));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,13 +22,22 @@ enum RTCQuality {
|
|||||||
RTCQualityGPS = 4
|
RTCQualityGPS = 4
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The RTC set result codes
|
||||||
|
/// Used to indicate the result of an attempt to set the RTC.
|
||||||
|
enum RTCSetResult {
|
||||||
|
RTCSetResultNotSet = 0, ///< RTC was set successfully
|
||||||
|
RTCSetResultSuccess = 1, ///< RTC was set successfully
|
||||||
|
RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch)
|
||||||
|
RTCSetResultError = 4 ///< An error occurred while setting the RTC
|
||||||
|
};
|
||||||
|
|
||||||
RTCQuality getRTCQuality();
|
RTCQuality getRTCQuality();
|
||||||
|
|
||||||
extern uint32_t lastSetFromPhoneNtpOrGps;
|
extern uint32_t lastSetFromPhoneNtpOrGps;
|
||||||
|
|
||||||
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
|
/// If we haven't yet set our RTC this boot, set it from a GPS derived time
|
||||||
bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
|
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
|
||||||
bool perhapsSetRTC(RTCQuality q, struct tm &t);
|
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t);
|
||||||
|
|
||||||
/// Return a string name for the quality
|
/// Return a string name for the quality
|
||||||
const char *RtcName(RTCQuality quality);
|
const char *RtcName(RTCQuality quality);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
|
|
||||||
|
#ifdef GXEPD2_DRIVER_0
|
||||||
|
#include "einkDetect.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini
|
The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini
|
||||||
Previously, these macros were defined at the top of this file.
|
Previously, these macros were defined at the top of this file.
|
||||||
@@ -136,17 +140,32 @@ bool EInkDisplay::connect()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1)
|
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||||
{
|
{
|
||||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
||||||
|
|
||||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||||
adafruitDisplay->init();
|
adafruitDisplay->init();
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||||
adafruitDisplay->setRotation(4);
|
adafruitDisplay->setRotation(4);
|
||||||
#else
|
#else
|
||||||
adafruitDisplay->setRotation(3);
|
adafruitDisplay->setRotation(3);
|
||||||
#endif
|
#endif
|
||||||
|
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
|
||||||
|
}
|
||||||
|
#elif defined(ELECROW_ThinkNode_M5)
|
||||||
|
{
|
||||||
|
// Start HSPI
|
||||||
|
hspi = new SPIClass(HSPI);
|
||||||
|
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
|
||||||
|
|
||||||
|
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
|
||||||
|
|
||||||
|
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||||
|
adafruitDisplay->init();
|
||||||
|
|
||||||
|
adafruitDisplay->setRotation(4);
|
||||||
|
|
||||||
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
|
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
|
||||||
}
|
}
|
||||||
#elif defined(MESHLINK)
|
#elif defined(MESHLINK)
|
||||||
@@ -174,9 +193,8 @@ bool EInkDisplay::connect()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \
|
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \
|
||||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
|
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
|
||||||
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
|
|
||||||
{
|
{
|
||||||
// Start HSPI
|
// Start HSPI
|
||||||
hspi = new SPIClass(HSPI);
|
hspi = new SPIClass(HSPI);
|
||||||
@@ -203,7 +221,7 @@ bool EInkDisplay::connect()
|
|||||||
adafruitDisplay->setRotation(0);
|
adafruitDisplay->setRotation(0);
|
||||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||||
}
|
}
|
||||||
#elif defined(M5_COREINK)
|
#elif defined(M5_COREINK) || defined(T_DECK_PRO)
|
||||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
|
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
|
||||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||||
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||||
@@ -232,6 +250,23 @@ bool EInkDisplay::connect()
|
|||||||
adafruitDisplay->init();
|
adafruitDisplay->init();
|
||||||
adafruitDisplay->setRotation(3);
|
adafruitDisplay->setRotation(3);
|
||||||
}
|
}
|
||||||
|
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
|
||||||
|
|
||||||
|
// Detect display model, before starting SPI
|
||||||
|
EInkDetectionResult displayModel = detectEInk();
|
||||||
|
|
||||||
|
// Start HSPI
|
||||||
|
hspi = new SPIClass(HSPI);
|
||||||
|
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
|
||||||
|
|
||||||
|
// Create GxEPD2 object
|
||||||
|
adafruitDisplay = new GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1>((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC,
|
||||||
|
PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
|
||||||
|
|
||||||
|
// Init GxEPD2
|
||||||
|
adafruitDisplay->init();
|
||||||
|
adafruitDisplay->setRotation(3);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
#include "GxEPD2_BW.h"
|
#include "GxEPD2_BW.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
|
||||||
|
#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models
|
||||||
|
#include "GxEPD2Multi.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
|
||||||
*
|
*
|
||||||
@@ -63,13 +67,20 @@ class EInkDisplay : public OLEDDisplay
|
|||||||
// Connect to the display
|
// Connect to the display
|
||||||
virtual bool connect() override;
|
virtual bool connect() override;
|
||||||
|
|
||||||
// AdafruitGFX display object - instantiated in connect(), variant specific
|
#ifdef GXEPD2_DRIVER_0
|
||||||
|
// AdafruitGFX display object - wrapper for multiple drivers
|
||||||
|
// Allows runtime detection of multiple displays
|
||||||
|
// Avoid this situation if possible!
|
||||||
|
GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1> *adafruitDisplay = NULL;
|
||||||
|
#else
|
||||||
|
// AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific
|
||||||
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL;
|
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL;
|
||||||
|
#endif
|
||||||
|
|
||||||
// If display uses HSPI
|
// If display uses HSPI
|
||||||
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
|
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
|
||||||
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
|
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
|
||||||
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
|
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5)
|
||||||
SPIClass *hspi = NULL;
|
SPIClass *hspi = NULL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
135
src/graphics/GxEPD2Multi.h
Normal file
135
src/graphics/GxEPD2Multi.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Wrapper class for GxEPD2_BW
|
||||||
|
|
||||||
|
// Generic signature at build-time, so that we can detect display model at run-time
|
||||||
|
// Workaround for issue of GxEPD2_BW objects not having a shared base class
|
||||||
|
// Only exposes methods which we are actually using
|
||||||
|
|
||||||
|
template <typename Driver0, typename Driver1> class GxEPD2_Multi
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->drawPixel(x, y, color);
|
||||||
|
else
|
||||||
|
driver1->drawPixel(x, y, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nextPage()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
return driver0->nextPage();
|
||||||
|
else
|
||||||
|
return driver1->nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void hibernate()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->hibernate();
|
||||||
|
else
|
||||||
|
driver1->hibernate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(uint32_t serial_diag_bitrate = 0)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->init(serial_diag_bitrate);
|
||||||
|
else
|
||||||
|
driver1->init(serial_diag_bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
|
||||||
|
else
|
||||||
|
driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRotation(uint8_t x)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->setRotation(x);
|
||||||
|
else
|
||||||
|
driver1->setRotation(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->setPartialWindow(x, y, w, h);
|
||||||
|
else
|
||||||
|
driver1->setPartialWindow(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFullWindow()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->setFullWindow();
|
||||||
|
else
|
||||||
|
driver1->setFullWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t width()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
return driver0->width();
|
||||||
|
else
|
||||||
|
return driver1->width();
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t height()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
return driver0->height();
|
||||||
|
else
|
||||||
|
return driver1->height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearScreen(uint8_t value = 0xFF)
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->clearScreen();
|
||||||
|
else
|
||||||
|
driver1->clearScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void endAsyncFull()
|
||||||
|
{
|
||||||
|
if (which == 0)
|
||||||
|
driver0->endAsyncFull();
|
||||||
|
else
|
||||||
|
driver1->endAsyncFull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd
|
||||||
|
class Epd2Wrapper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool isBusy() { return m_epd2->isBusy(); }
|
||||||
|
GxEPD2_EPD *m_epd2;
|
||||||
|
} epd2;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
// Select driver by passing whichDriver as 0 or 1
|
||||||
|
GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi)
|
||||||
|
{
|
||||||
|
assert(whichDriver == 0 || whichDriver == 1);
|
||||||
|
which = whichDriver;
|
||||||
|
LOG_DEBUG("GxEPD2_Multi driver: %d", which);
|
||||||
|
|
||||||
|
if (which == 0) {
|
||||||
|
driver0 = new GxEPD2_BW<Driver0, Driver0::HEIGHT>(Driver0(cs, dc, rst, busy, spi));
|
||||||
|
epd2.m_epd2 = &(driver0->epd2);
|
||||||
|
} else if (which == 1) {
|
||||||
|
driver1 = new GxEPD2_BW<Driver1, Driver1::HEIGHT>(Driver1(cs, dc, rst, busy, spi));
|
||||||
|
epd2.m_epd2 = &(driver1->epd2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t which;
|
||||||
|
GxEPD2_BW<Driver0, Driver0::HEIGHT> *driver0;
|
||||||
|
GxEPD2_BW<Driver1, Driver1::HEIGHT> *driver1;
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
#include "Screen.h"
|
#include "Screen.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
@@ -31,6 +32,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "TimeFormatters.h"
|
#include "TimeFormatters.h"
|
||||||
#include "draw/ClockRenderer.h"
|
#include "draw/ClockRenderer.h"
|
||||||
#include "draw/DebugRenderer.h"
|
#include "draw/DebugRenderer.h"
|
||||||
|
#include "draw/MenuHandler.h"
|
||||||
#include "draw/MessageRenderer.h"
|
#include "draw/MessageRenderer.h"
|
||||||
#include "draw/NodeListRenderer.h"
|
#include "draw/NodeListRenderer.h"
|
||||||
#include "draw/NotificationRenderer.h"
|
#include "draw/NotificationRenderer.h"
|
||||||
@@ -43,7 +45,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#endif
|
#endif
|
||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
|
||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
@@ -68,6 +69,8 @@ using graphics::Emote;
|
|||||||
using graphics::emotes;
|
using graphics::emotes;
|
||||||
using graphics::numEmotes;
|
using graphics::numEmotes;
|
||||||
|
|
||||||
|
extern uint16_t TFT_MESH;
|
||||||
|
|
||||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
#include "mesh/wifi/WiFiAPClient.h"
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -134,21 +137,120 @@ extern bool hasUnreadMessage;
|
|||||||
// Displays a temporary centered banner message (e.g., warning, status, etc.)
|
// Displays a temporary centered banner message (e.g., warning, status, etc.)
|
||||||
// The banner appears in the center of the screen and disappears after the specified duration
|
// The banner appears in the center of the screen and disappears after the specified duration
|
||||||
|
|
||||||
// Called to trigger a banner with custom message and duration
|
void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
|
||||||
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function<void(int)> bannerCallback,
|
|
||||||
int8_t InitialSelected)
|
|
||||||
{
|
{
|
||||||
|
BannerOverlayOptions options;
|
||||||
|
options.message = message;
|
||||||
|
options.durationMs = durationMs;
|
||||||
|
options.notificationType = notificationTypeEnum::text_banner;
|
||||||
|
showOverlayBanner(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called to trigger a banner with custom message and duration
|
||||||
|
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
|
// Store the message and set the expiration timestamp
|
||||||
|
strncpy(NotificationRenderer::alertBannerMessage, banner_overlay_options.message, 255);
|
||||||
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
|
NotificationRenderer::alertBannerUntil =
|
||||||
|
(banner_overlay_options.durationMs == 0) ? 0 : millis() + banner_overlay_options.durationMs;
|
||||||
|
NotificationRenderer::optionsArrayPtr = banner_overlay_options.optionsArrayPtr;
|
||||||
|
NotificationRenderer::optionsEnumPtr = banner_overlay_options.optionsEnumPtr;
|
||||||
|
NotificationRenderer::alertBannerOptions = banner_overlay_options.optionsCount;
|
||||||
|
NotificationRenderer::alertBannerCallback = banner_overlay_options.bannerCallback;
|
||||||
|
NotificationRenderer::curSelected = banner_overlay_options.InitialSelected;
|
||||||
|
NotificationRenderer::pauseBanner = false;
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::selection_picker;
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
ui->setTargetFPS(60);
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called to trigger a banner with custom message and duration
|
||||||
|
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
|
nodeDB->pause_sort(true);
|
||||||
// Store the message and set the expiration timestamp
|
// Store the message and set the expiration timestamp
|
||||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
NotificationRenderer::alertBannerOptions = options;
|
|
||||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
NotificationRenderer::curSelected = InitialSelected;
|
|
||||||
NotificationRenderer::pauseBanner = false;
|
NotificationRenderer::pauseBanner = false;
|
||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay};
|
NotificationRenderer::curSelected = 0;
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::node_picker;
|
||||||
|
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
setFastFramerate(); // Draw ASAP
|
ui->setTargetFPS(60);
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called to trigger a banner with custom message and duration
|
||||||
|
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||||
|
std::function<void(uint32_t)> bannerCallback)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
|
// Store the message and set the expiration timestamp
|
||||||
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||||
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
|
NotificationRenderer::pauseBanner = false;
|
||||||
|
NotificationRenderer::curSelected = 0;
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||||
|
NotificationRenderer::numDigits = digits;
|
||||||
|
NotificationRenderer::currentNumber = 0;
|
||||||
|
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
ui->setTargetFPS(60);
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
|
std::function<void(const std::string &)> textCallback)
|
||||||
|
{
|
||||||
|
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
|
||||||
|
|
||||||
|
if (NotificationRenderer::virtualKeyboard) {
|
||||||
|
delete NotificationRenderer::virtualKeyboard;
|
||||||
|
NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
|
||||||
|
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
|
||||||
|
if (header) {
|
||||||
|
NotificationRenderer::virtualKeyboard->setHeader(header);
|
||||||
|
}
|
||||||
|
if (initialText) {
|
||||||
|
NotificationRenderer::virtualKeyboard->setInputText(initialText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up callback with safer cleanup mechanism
|
||||||
|
NotificationRenderer::textInputCallback = textCallback;
|
||||||
|
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
|
||||||
|
|
||||||
|
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||||
|
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||||
|
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||||
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
|
NotificationRenderer::pauseBanner = false;
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||||
|
|
||||||
|
// Set the overlay using the same pattern as other notification types
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
ui->setTargetFPS(60);
|
||||||
ui->update();
|
ui->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +305,7 @@ float Screen::estimatedHeading(double lat, double lon)
|
|||||||
if (d < 10) // haven't moved enough, just keep current bearing
|
if (d < 10) // haven't moved enough, just keep current bearing
|
||||||
return b;
|
return b;
|
||||||
|
|
||||||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon);
|
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
||||||
oldLat = lat;
|
oldLat = lat;
|
||||||
oldLon = lon;
|
oldLon = lon;
|
||||||
|
|
||||||
@@ -225,6 +327,20 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
|
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
|
||||||
{
|
{
|
||||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||||
|
|
||||||
|
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
|
||||||
|
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||||
|
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||||
|
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
|
||||||
|
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
|
||||||
|
uint8_t TFT_MESH_b = rawRGB & 0xFF;
|
||||||
|
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||||
|
|
||||||
|
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
|
||||||
|
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
@@ -234,8 +350,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
ST7789_MISO, ST7789_SCK);
|
ST7789_MISO, ST7789_SCK);
|
||||||
#else
|
#else
|
||||||
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||||
static_cast<ST7789Spi *>(dispdev)->setRGB(COLOR565(255, 255, 128));
|
|
||||||
#endif
|
#endif
|
||||||
|
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||||
#elif defined(USE_SSD1306)
|
#elif defined(USE_SSD1306)
|
||||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||||
@@ -287,9 +403,6 @@ void Screen::doDeepSleep()
|
|||||||
{
|
{
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
|
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
|
||||||
#ifdef PIN_EINK_EN
|
|
||||||
digitalWrite(PIN_EINK_EN, LOW); // power off backlight
|
|
||||||
#endif
|
|
||||||
#else
|
#else
|
||||||
// Without E-Ink display:
|
// Without E-Ink display:
|
||||||
setOn(false);
|
setOn(false);
|
||||||
@@ -308,13 +421,19 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||||||
#endif
|
#endif
|
||||||
#ifdef HELTEC_TRACKER_V1_X
|
|
||||||
uint8_t tft_vext_enabled = digitalRead(VEXT_ENABLE);
|
|
||||||
#endif
|
|
||||||
#if !ARCH_PORTDUINO
|
#if !ARCH_PORTDUINO
|
||||||
dispdev->displayOn();
|
dispdev->displayOn();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
if (uiconfig.screen_brightness == 1)
|
||||||
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
|
#elif defined(PCA_PIN_EINK_EN)
|
||||||
|
if (uiconfig.screen_brightness == 1)
|
||||||
|
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(ST7789_CS) && \
|
#if defined(ST7789_CS) && \
|
||||||
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
|
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
|
||||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||||
@@ -322,10 +441,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
|
|
||||||
dispdev->displayOn();
|
dispdev->displayOn();
|
||||||
#ifdef HELTEC_TRACKER_V1_X
|
#ifdef HELTEC_TRACKER_V1_X
|
||||||
// If the TFT VEXT power is not enabled, initialize the UI.
|
ui->init();
|
||||||
if (!tft_vext_enabled) {
|
|
||||||
ui->init();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ST7789
|
#ifdef USE_ST7789
|
||||||
pinMode(VTFT_CTRL, OUTPUT);
|
pinMode(VTFT_CTRL, OUTPUT);
|
||||||
@@ -347,11 +463,13 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
|
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
|
||||||
setScreensaverFrames(einkScreensaver);
|
setScreensaverFrames(einkScreensaver);
|
||||||
#endif
|
#endif
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
|
||||||
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
#ifdef PIN_EINK_EN
|
||||||
digitalWrite(PIN_EINK_EN, LOW);
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
}
|
#elif defined(PCA_PIN_EINK_EN)
|
||||||
|
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
dispdev->displayOff();
|
dispdev->displayOff();
|
||||||
#ifdef USE_ST7789
|
#ifdef USE_ST7789
|
||||||
SPI1.end();
|
SPI1.end();
|
||||||
@@ -381,9 +499,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
|
|
||||||
void Screen::setup()
|
void Screen::setup()
|
||||||
{
|
{
|
||||||
|
|
||||||
// === Enable display rendering ===
|
// === Enable display rendering ===
|
||||||
useDisplay = true;
|
useDisplay = true;
|
||||||
|
|
||||||
|
// === Load saved brightness from UI config ===
|
||||||
|
// For OLED displays (SSD1306), default brightness is 255 if not set
|
||||||
|
if (uiconfig.screen_brightness == 0) {
|
||||||
|
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||||
|
brightness = 255; // Default for OLED
|
||||||
|
#else
|
||||||
|
brightness = BRIGHTNESS_DEFAULT;
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
brightness = uiconfig.screen_brightness;
|
||||||
|
}
|
||||||
|
|
||||||
// === Detect OLED subtype (if supported by board variant) ===
|
// === Detect OLED subtype (if supported by board variant) ===
|
||||||
#ifdef AutoOLEDWire_h
|
#ifdef AutoOLEDWire_h
|
||||||
if (isAUTOOled)
|
if (isAUTOOled)
|
||||||
@@ -411,10 +542,17 @@ void Screen::setup()
|
|||||||
ui->disableAllIndicators(); // Disable page indicator dots
|
ui->disableAllIndicators(); // Disable page indicator dots
|
||||||
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
||||||
|
|
||||||
|
// === Apply loaded brightness ===
|
||||||
|
#if defined(ST7789_CS)
|
||||||
|
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||||
|
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||||
|
dispdev->setBrightness(brightness);
|
||||||
|
#endif
|
||||||
|
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||||
|
|
||||||
// === Set custom overlay callbacks ===
|
// === Set custom overlay callbacks ===
|
||||||
static OverlayCallback overlays[] = {
|
static OverlayCallback overlays[] = {
|
||||||
graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc.
|
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
|
||||||
};
|
};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
@@ -471,6 +609,7 @@ void Screen::setup()
|
|||||||
|
|
||||||
// === Turn on display and trigger first draw ===
|
// === Turn on display and trigger first draw ===
|
||||||
handleSetOn(true);
|
handleSetOn(true);
|
||||||
|
determineResolution(dispdev->height(), dispdev->width());
|
||||||
ui->update();
|
ui->update();
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||||||
@@ -485,7 +624,7 @@ void Screen::setup()
|
|||||||
touchScreenImpl1->init();
|
touchScreenImpl1->init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#elif HAS_TOUCHSCREEN
|
#elif HAS_TOUCHSCREEN && !defined(USE_EINK)
|
||||||
touchScreenImpl1 =
|
touchScreenImpl1 =
|
||||||
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
||||||
touchScreenImpl1->init();
|
touchScreenImpl1->init();
|
||||||
@@ -541,6 +680,11 @@ void Screen::forceDisplay(bool forceUiUpdate)
|
|||||||
|
|
||||||
// Tell EInk class to update the display
|
// Tell EInk class to update the display
|
||||||
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
||||||
|
#else
|
||||||
|
// No delay between UI frame rendering
|
||||||
|
if (forceUiUpdate) {
|
||||||
|
setFastFramerate();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,6 +701,7 @@ int32_t Screen::runOnce()
|
|||||||
if (displayHeight == 0) {
|
if (displayHeight == 0) {
|
||||||
displayHeight = dispdev->getHeight();
|
displayHeight = dispdev->getHeight();
|
||||||
}
|
}
|
||||||
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
|
|
||||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||||||
@@ -585,11 +730,11 @@ int32_t Screen::runOnce()
|
|||||||
|
|
||||||
#ifndef DISABLE_WELCOME_UNSET
|
#ifndef DISABLE_WELCOME_UNSET
|
||||||
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||||
LoraRegionPicker(0);
|
menuHandler::OnboardMessage();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||||
showOverlayBanner("Rebooting...", 0);
|
showSimpleBanner("Rebooting...", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process incoming commands.
|
// Process incoming commands.
|
||||||
@@ -606,13 +751,19 @@ int32_t Screen::runOnce()
|
|||||||
handleSetOn(false);
|
handleSetOn(false);
|
||||||
break;
|
break;
|
||||||
case Cmd::ON_PRESS:
|
case Cmd::ON_PRESS:
|
||||||
handleOnPress();
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
|
handleOnPress();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_PREV_FRAME:
|
case Cmd::SHOW_PREV_FRAME:
|
||||||
handleShowPrevFrame();
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
|
handleShowPrevFrame();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_NEXT_FRAME:
|
case Cmd::SHOW_NEXT_FRAME:
|
||||||
handleShowNextFrame();
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
|
handleShowNextFrame();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::START_ALERT_FRAME: {
|
case Cmd::START_ALERT_FRAME: {
|
||||||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||||||
@@ -634,7 +785,11 @@ int32_t Screen::runOnce()
|
|||||||
NotificationRenderer::pauseBanner = false;
|
NotificationRenderer::pauseBanner = false;
|
||||||
case Cmd::STOP_BOOT_SCREEN:
|
case Cmd::STOP_BOOT_SCREEN:
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||||||
setFrames();
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
|
setFrames();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Cmd::NOOP:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_ERROR("Invalid screen cmd");
|
LOG_ERROR("Invalid screen cmd");
|
||||||
@@ -668,6 +823,7 @@ int32_t Screen::runOnce()
|
|||||||
if (showingNormalScreen) {
|
if (showingNormalScreen) {
|
||||||
// standard screen loop handling here
|
// standard screen loop handling here
|
||||||
if (config.display.auto_screen_carousel_secs > 0 &&
|
if (config.display.auto_screen_carousel_secs > 0 &&
|
||||||
|
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
|
||||||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||||||
|
|
||||||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||||||
@@ -758,42 +914,23 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
|||||||
// Called when a frame should be added / removed, or custom frames should be cleared
|
// Called when a frame should be added / removed, or custom frames should be cleared
|
||||||
void Screen::setFrames(FrameFocus focus)
|
void Screen::setFrames(FrameFocus focus)
|
||||||
{
|
{
|
||||||
|
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
|
||||||
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||||||
uint8_t previousFrameCount = framesetInfo.frameCount;
|
uint8_t previousFrameCount = framesetInfo.frameCount;
|
||||||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||||||
|
|
||||||
|
graphics::UIRenderer::rebuildFavoritedNodes();
|
||||||
|
|
||||||
LOG_DEBUG("Show standard frames");
|
LOG_DEBUG("Show standard frames");
|
||||||
showingNormalScreen = true;
|
showingNormalScreen = true;
|
||||||
|
|
||||||
indicatorIcons.clear();
|
indicatorIcons.clear();
|
||||||
|
|
||||||
size_t numframes = 0;
|
size_t numframes = 0;
|
||||||
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
|
|
||||||
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
|
||||||
|
|
||||||
// put all of the module frames first.
|
|
||||||
// this is a little bit of a dirty hack; since we're going to call
|
|
||||||
// the same drawModuleFrame handler here for all of these module frames
|
|
||||||
// and then we'll just assume that the state->currentFrame value
|
|
||||||
// is the same offset into the moduleFrames vector
|
|
||||||
// so that we can invoke the module's callback
|
|
||||||
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
|
||||||
// Draw the module frame, using the hack described above
|
|
||||||
normalFrames[numframes] = drawModuleFrame;
|
|
||||||
|
|
||||||
// Check if the module being drawn has requested focus
|
|
||||||
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
|
||||||
MeshModule *m = *i;
|
|
||||||
if (m->isRequestingFocus())
|
|
||||||
fsi.positions.focusedModule = numframes;
|
|
||||||
if (m == waypointModule)
|
|
||||||
fsi.positions.waypoint = numframes;
|
|
||||||
|
|
||||||
indicatorIcons.push_back(icon_module);
|
|
||||||
numframes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
|
||||||
|
|
||||||
// If we have a critical fault, show it first
|
// If we have a critical fault, show it first
|
||||||
fsi.positions.fault = numframes;
|
fsi.positions.fault = numframes;
|
||||||
@@ -805,9 +942,9 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
|
|
||||||
#if defined(DISPLAY_CLOCK_FRAME)
|
#if defined(DISPLAY_CLOCK_FRAME)
|
||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
|
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||||
: &graphics::ClockRenderer::drawAnalogClockFrame;
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||||
indicatorIcons.push_back(icon_clock);
|
indicatorIcons.push_back(digital_icon_clock);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||||
@@ -822,22 +959,27 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
indicatorIcons.push_back(icon_mail);
|
indicatorIcons.push_back(icon_mail);
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
|
fsi.positions.nodelist = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||||
indicatorIcons.push_back(icon_nodes);
|
indicatorIcons.push_back(icon_nodes);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Show detailed node views only on E-Ink builds
|
// Show detailed node views only on E-Ink builds
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
|
fsi.positions.nodelist_lastheard = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||||
indicatorIcons.push_back(icon_nodes);
|
indicatorIcons.push_back(icon_nodes);
|
||||||
|
|
||||||
|
fsi.positions.nodelist_hopsignal = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||||
indicatorIcons.push_back(icon_signal);
|
indicatorIcons.push_back(icon_signal);
|
||||||
|
|
||||||
|
fsi.positions.nodelist_distance = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||||||
indicatorIcons.push_back(icon_distance);
|
indicatorIcons.push_back(icon_distance);
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
|
fsi.positions.nodelist_bearings = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||||
indicatorIcons.push_back(icon_list);
|
indicatorIcons.push_back(icon_list);
|
||||||
|
|
||||||
@@ -857,26 +999,11 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#if !defined(DISPLAY_CLOCK_FRAME)
|
#if !defined(DISPLAY_CLOCK_FRAME)
|
||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame;
|
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||||
indicatorIcons.push_back(icon_clock);
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||||
|
indicatorIcons.push_back(digital_icon_clock);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We don't show the node info of our node (if we have it yet - we should)
|
|
||||||
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
|
||||||
if (numMeshNodes > 0)
|
|
||||||
numMeshNodes--;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
|
||||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
|
||||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
|
||||||
if (fsi.positions.firstFavorite == 255)
|
|
||||||
fsi.positions.firstFavorite = numframes;
|
|
||||||
fsi.positions.lastFavorite = numframes;
|
|
||||||
normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo;
|
|
||||||
indicatorIcons.push_back(icon_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
if (!dismissedFrames.wifi && isWifiAvailable()) {
|
if (!dismissedFrames.wifi && isWifiAvailable()) {
|
||||||
fsi.positions.wifi = numframes;
|
fsi.positions.wifi = numframes;
|
||||||
@@ -885,6 +1012,64 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Beware of what changes you make in this code!
|
||||||
|
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important!
|
||||||
|
// Inside of that callback, goes over to MeshModule.cpp and we run
|
||||||
|
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
|
||||||
|
// entries until we're ready to start building the matching entries.
|
||||||
|
// We are doing our best to keep the normalFrames vector
|
||||||
|
// and the moduleFrames vector in lock step.
|
||||||
|
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
|
||||||
|
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
||||||
|
|
||||||
|
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
||||||
|
// Draw the module frame, using the hack described above
|
||||||
|
if (*i != nullptr) {
|
||||||
|
normalFrames[numframes] = drawModuleFrame;
|
||||||
|
|
||||||
|
// Check if the module being drawn has requested focus
|
||||||
|
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
||||||
|
MeshModule *m = *i;
|
||||||
|
if (m && m->isRequestingFocus())
|
||||||
|
fsi.positions.focusedModule = numframes;
|
||||||
|
if (m && m == waypointModule)
|
||||||
|
fsi.positions.waypoint = numframes;
|
||||||
|
|
||||||
|
indicatorIcons.push_back(icon_module);
|
||||||
|
numframes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
||||||
|
|
||||||
|
// We don't show the node info of our node (if we have it yet - we should)
|
||||||
|
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
||||||
|
if (numMeshNodes > 0)
|
||||||
|
numMeshNodes--;
|
||||||
|
|
||||||
|
// Temporary array to hold favorite node frames
|
||||||
|
std::vector<FrameCallback> favoriteFrames;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||||
|
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||||
|
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert favorite frames *after* collecting them all
|
||||||
|
if (!favoriteFrames.empty()) {
|
||||||
|
fsi.positions.firstFavorite = numframes;
|
||||||
|
for (const auto &f : favoriteFrames) {
|
||||||
|
normalFrames[numframes++] = f;
|
||||||
|
indicatorIcons.push_back(icon_node);
|
||||||
|
}
|
||||||
|
fsi.positions.lastFavorite = numframes - 1;
|
||||||
|
} else {
|
||||||
|
fsi.positions.firstFavorite = 255;
|
||||||
|
fsi.positions.lastFavorite = 255;
|
||||||
|
}
|
||||||
|
|
||||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||||
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
||||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||||
@@ -893,11 +1078,10 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
ui->disableAllIndicators();
|
ui->disableAllIndicators();
|
||||||
|
|
||||||
// Add overlays: frame icons and alert banner)
|
// Add overlays: frame icons and alert banner)
|
||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay};
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list
|
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||||
// just changed)
|
|
||||||
|
|
||||||
// Focus on a specific frame, in the frame set we just created
|
// Focus on a specific frame, in the frame set we just created
|
||||||
switch (focus) {
|
switch (focus) {
|
||||||
@@ -916,6 +1100,14 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
// If no module requested focus, will show the first frame instead
|
// If no module requested focus, will show the first frame instead
|
||||||
ui->switchToFrame(fsi.positions.focusedModule);
|
ui->switchToFrame(fsi.positions.focusedModule);
|
||||||
break;
|
break;
|
||||||
|
case FOCUS_CLOCK:
|
||||||
|
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
||||||
|
// If no module requested focus, will show the first frame instead
|
||||||
|
ui->switchToFrame(fsi.positions.clock);
|
||||||
|
break;
|
||||||
|
case FOCUS_SYSTEM:
|
||||||
|
ui->switchToFrame(fsi.positions.memory);
|
||||||
|
break;
|
||||||
|
|
||||||
case FOCUS_PRESERVE:
|
case FOCUS_PRESERVE:
|
||||||
// No more adjustment — force stay on same index
|
// No more adjustment — force stay on same index
|
||||||
@@ -1126,8 +1318,12 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||||
hasUnreadMessage = true; // Enables mail icon in the header
|
hasUnreadMessage = true; // Enables mail icon in the header
|
||||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||||
forceDisplay(); // Forces screen redraw
|
|
||||||
|
|
||||||
|
// Only wake/force display if the configuration allows it
|
||||||
|
if (shouldWakeOnReceivedMessage()) {
|
||||||
|
setOn(true); // Wake up the screen first
|
||||||
|
forceDisplay(); // Forces screen redraw
|
||||||
|
}
|
||||||
// === Prepare banner content ===
|
// === Prepare banner content ===
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||||
@@ -1159,7 +1355,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->showOverlayBanner(banner, 3000);
|
screen->showSimpleBanner(banner, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
// Triggered by MeshModules
|
// Triggered by MeshModules
|
||||||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||||||
{
|
{
|
||||||
|
// Block UI frame events when virtual keyboard is active
|
||||||
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (showingNormalScreen) {
|
if (showingNormalScreen) {
|
||||||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||||||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||||||
@@ -1191,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
if (!screenOn)
|
if (!screenOn)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
// Handle text input notifications specially - pass input to virtual keyboard
|
||||||
|
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||||
|
NotificationRenderer::inEvent = *event;
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
setFastFramerate(); // Draw ASAP
|
||||||
|
ui->update();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
|
||||||
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
|
||||||
@@ -1198,29 +1409,15 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
setFastFramerate(); // Draw ASAP
|
setFastFramerate(); // Draw ASAP
|
||||||
#endif
|
#endif
|
||||||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||||||
NotificationRenderer::inEvent = event->inputEvent;
|
NotificationRenderer::inEvent = *event;
|
||||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar,
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
NotificationRenderer::drawAlertBannerOverlay};
|
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
setFastFramerate(); // Draw ASAP
|
setFastFramerate(); // Draw ASAP
|
||||||
ui->update();
|
ui->update();
|
||||||
|
|
||||||
|
menuHandler::handleMenuSwitch(dispdev);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
#if defined(DISPLAY_CLOCK_FRAME)
|
|
||||||
// For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button
|
|
||||||
uint8_t watchFaceFrame = error_code ? 1 : 0;
|
|
||||||
|
|
||||||
if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 &&
|
|
||||||
event->touchY >= 204 && event->touchY <= 240) {
|
|
||||||
screen->digitalWatchFace = !screen->digitalWatchFace;
|
|
||||||
|
|
||||||
setFrames();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Use left or right input from a keyboard to move between frames,
|
// Use left or right input from a keyboard to move between frames,
|
||||||
// so long as a mesh module isn't using these events for some other purpose
|
// so long as a mesh module isn't using these events for some other purpose
|
||||||
@@ -1229,7 +1426,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
// Ask any MeshModules if they're handling keyboard input right now
|
// Ask any MeshModules if they're handling keyboard input right now
|
||||||
bool inputIntercepted = false;
|
bool inputIntercepted = false;
|
||||||
for (MeshModule *module : moduleFrames) {
|
for (MeshModule *module : moduleFrames) {
|
||||||
if (module->interceptingKeyboardInput())
|
if (module && module->interceptingKeyboardInput())
|
||||||
inputIntercepted = true;
|
inputIntercepted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,129 +1438,36 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
showNextFrame();
|
showNextFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||||
const char *banner_message;
|
menuHandler::homeBaseMenu();
|
||||||
int options;
|
|
||||||
if (kb_found) {
|
|
||||||
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg";
|
|
||||||
options = 4;
|
|
||||||
} else {
|
|
||||||
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg";
|
|
||||||
options = 3;
|
|
||||||
}
|
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
screen->setOn(false);
|
|
||||||
} else if (selected == 2) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
||||||
} else if (selected == 3) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#if HAS_TFT
|
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
||||||
showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void {
|
menuHandler::systemBaseMenu();
|
||||||
if (selected == 0) {
|
|
||||||
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
|
||||||
config.bluetooth.enabled = false;
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#else
|
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
|
||||||
showOverlayBanner(
|
|
||||||
"Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4,
|
|
||||||
[](int selected) -> void {
|
|
||||||
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
},
|
|
||||||
config.device.buzzer_mode);
|
|
||||||
#endif
|
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
||||||
showOverlayBanner(
|
menuHandler::positionBaseMenu();
|
||||||
"Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3,
|
|
||||||
[](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
|
||||||
playGPSEnableBeep();
|
|
||||||
gps->enable();
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
} else if (selected == 2) {
|
|
||||||
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
|
|
||||||
playGPSDisableBeep();
|
|
||||||
gps->disable();
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1
|
|
||||||
: 2); // set inital selection
|
|
||||||
#endif
|
#endif
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
||||||
TZPicker();
|
menuHandler::clockMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||||
LoraRegionPicker();
|
menuHandler::LoraRegionPicker();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||||
devicestate.rx_text_message.from) {
|
if (devicestate.rx_text_message.from) {
|
||||||
const char *banner_message;
|
menuHandler::messageResponseMenu();
|
||||||
int options;
|
|
||||||
if (kb_found) {
|
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext";
|
|
||||||
options = 4;
|
|
||||||
} else {
|
} else {
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset";
|
menuHandler::textMessageBaseMenu();
|
||||||
options = 3;
|
|
||||||
}
|
}
|
||||||
#ifdef HAS_I2S
|
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud";
|
|
||||||
options = 5;
|
|
||||||
#endif
|
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
screen->dismissCurrentFrame();
|
|
||||||
} else if (selected == 2) {
|
|
||||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST,
|
|
||||||
devicestate.rx_text_message.channel);
|
|
||||||
} else {
|
|
||||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
|
||||||
}
|
|
||||||
} else if (selected == 3) {
|
|
||||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST,
|
|
||||||
devicestate.rx_text_message.channel);
|
|
||||||
} else {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef HAS_I2S
|
|
||||||
else if (selected == 4) {
|
|
||||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
|
||||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
|
||||||
|
|
||||||
audioThread->readAloud(msg);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
});
|
|
||||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||||
const char *banner_message;
|
menuHandler::favoriteBaseMenu();
|
||||||
int options;
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
|
||||||
if (kb_found) {
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||||
banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg";
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||||
options = 3;
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||||
} else {
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||||
banner_message = "Message Node?\nCancel\nConfirm";
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
||||||
options = 2;
|
menuHandler::nodeListMenu();
|
||||||
}
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) {
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
menuHandler::wifiBaseMenu();
|
||||||
if (selected == 1) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
|
||||||
} else if (selected == 2) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||||
showPrevFrame();
|
showPrevFrame();
|
||||||
@@ -1397,98 +1501,28 @@ bool Screen::isOverlayBannerShowing()
|
|||||||
return NotificationRenderer::isOverlayBannerShowing();
|
return NotificationRenderer::isOverlayBannerShowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::LoraRegionPicker(uint32_t duration)
|
|
||||||
{
|
|
||||||
showOverlayBanner(
|
|
||||||
"Set the LoRa "
|
|
||||||
"region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_"
|
|
||||||
"919\nSG_"
|
|
||||||
"923\nPH_433\nPH_868\nPH_915\nANZ_433",
|
|
||||||
duration, 23,
|
|
||||||
[](int selected) -> void {
|
|
||||||
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
|
||||||
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
|
||||||
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
|
||||||
if (!owner.is_licensed) {
|
|
||||||
bool keygenSuccess = false;
|
|
||||||
if (config.security.private_key.size == 32) {
|
|
||||||
// public key is derived from private, so this will always have the same result.
|
|
||||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
||||||
keygenSuccess = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_INFO("Generate new PKI keys");
|
|
||||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
||||||
keygenSuccess = true;
|
|
||||||
}
|
|
||||||
if (keygenSuccess) {
|
|
||||||
config.security.public_key.size = 32;
|
|
||||||
config.security.private_key.size = 32;
|
|
||||||
owner.public_key.size = 32;
|
|
||||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.lora.tx_enabled = true;
|
|
||||||
initRegion();
|
|
||||||
if (myRegion->dutyCycle < 100) {
|
|
||||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
|
||||||
}
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Screen::TZPicker()
|
|
||||||
{
|
|
||||||
showOverlayBanner(
|
|
||||||
"Pick "
|
|
||||||
"Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/"
|
|
||||||
"Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ",
|
|
||||||
30000, 17, [](int selected) -> void {
|
|
||||||
if (selected == 1) { // Hawaii
|
|
||||||
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 2) { // Alaska
|
|
||||||
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 3) { // Pacific
|
|
||||||
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 4) { // Mountain
|
|
||||||
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 5) { // Central
|
|
||||||
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 6) { // Eastern
|
|
||||||
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 7) { // UTC
|
|
||||||
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 8) { // EU/Western
|
|
||||||
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 9) { // EU/Central
|
|
||||||
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 10) { // EU/Eastern
|
|
||||||
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 11) { // Asia/Kolkata
|
|
||||||
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 12) { // China
|
|
||||||
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 13) { // AU/AWST
|
|
||||||
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 14) { // AU/ACST
|
|
||||||
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 15) { // AU/AEST
|
|
||||||
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 16) { // NZ
|
|
||||||
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
}
|
|
||||||
if (selected != 0) {
|
|
||||||
setenv("TZ", config.device.tzdef, 1);
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
#else
|
#else
|
||||||
graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {}
|
graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {}
|
||||||
#endif // HAS_SCREEN
|
#endif // HAS_SCREEN
|
||||||
|
|
||||||
|
bool shouldWakeOnReceivedMessage()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
The goal here is to determine when we do NOT wake up the screen on message received:
|
||||||
|
- Any ext. notifications are turned on
|
||||||
|
- If role is not client / client_mute
|
||||||
|
- If the battery level is very low
|
||||||
|
*/
|
||||||
|
if (moduleConfig.external_notification.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,28 @@
|
|||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "mesh/generated/meshtastic/config.pb.h"
|
#include "mesh/generated/meshtastic/config.pb.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||||
|
|
||||||
|
struct BannerOverlayOptions {
|
||||||
|
const char *message;
|
||||||
|
uint32_t durationMs = 30000;
|
||||||
|
const char **optionsArrayPtr = nullptr;
|
||||||
|
const int *optionsEnumPtr = nullptr;
|
||||||
|
uint8_t optionsCount = 0;
|
||||||
|
std::function<void(int)> bannerCallback = nullptr;
|
||||||
|
int8_t InitialSelected = 0;
|
||||||
|
notificationTypeEnum notificationType = notificationTypeEnum::text_banner;
|
||||||
|
};
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
bool shouldWakeOnReceivedMessage();
|
||||||
|
|
||||||
#if !HAS_SCREEN
|
#if !HAS_SCREEN
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -24,6 +42,8 @@ class Screen
|
|||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
FOCUS_TEXTMESSAGE,
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
|
FOCUS_SYSTEM,
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
@@ -38,10 +58,8 @@ class Screen
|
|||||||
void setFunctionSymbol(std::string) {}
|
void setFunctionSymbol(std::string) {}
|
||||||
void removeFunctionSymbol(std::string) {}
|
void removeFunctionSymbol(std::string) {}
|
||||||
void startAlert(const char *) {}
|
void startAlert(const char *) {}
|
||||||
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
|
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
|
||||||
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0)
|
void showOverlayBanner(BannerOverlayOptions) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
void setFrames(FrameFocus focus) {}
|
void setFrames(FrameFocus focus) {}
|
||||||
void endAlert() {}
|
void endAlert() {}
|
||||||
};
|
};
|
||||||
@@ -76,6 +94,7 @@ class Screen
|
|||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
#include "concurrency/LockGuard.h"
|
#include "concurrency/LockGuard.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "graphics/draw/MenuHandler.h"
|
||||||
#include "input/InputBroker.h"
|
#include "input/InputBroker.h"
|
||||||
#include "mesh/MeshModule.h"
|
#include "mesh/MeshModule.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
@@ -198,6 +217,7 @@ class Screen : public concurrency::OSThread
|
|||||||
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
|
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
OLEDDisplay *getDisplayDevice() { return dispdev; }
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
size_t frameCount = 0; // Total number of active frames
|
size_t frameCount = 0; // Total number of active frames
|
||||||
~Screen();
|
~Screen();
|
||||||
@@ -209,6 +229,8 @@ class Screen : public concurrency::OSThread
|
|||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
FOCUS_TEXTMESSAGE,
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
|
FOCUS_SYSTEM,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Regenerate the normal set of frames, focusing a specific frame if requested
|
// Regenerate the normal set of frames, focusing a specific frame if requested
|
||||||
@@ -286,8 +308,19 @@ class Screen : public concurrency::OSThread
|
|||||||
enqueueCmd(cmd);
|
enqueueCmd(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
|
void showSimpleBanner(const char *message, uint32_t durationMs = 0);
|
||||||
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0);
|
void showOverlayBanner(BannerOverlayOptions);
|
||||||
|
|
||||||
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||||
|
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||||
|
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
|
std::function<void(const std::string &)> textCallback);
|
||||||
|
|
||||||
|
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||||
|
{
|
||||||
|
graphics::menuHandler::menuQueue = menuToShow;
|
||||||
|
runNow();
|
||||||
|
}
|
||||||
|
|
||||||
void startFirmwareUpdateScreen()
|
void startFirmwareUpdateScreen()
|
||||||
{
|
{
|
||||||
@@ -301,7 +334,7 @@ class Screen : public concurrency::OSThread
|
|||||||
void setHeading(long _heading)
|
void setHeading(long _heading)
|
||||||
{
|
{
|
||||||
hasCompass = true;
|
hasCompass = true;
|
||||||
compassHeading = _heading;
|
compassHeading = fmod(_heading, 360);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasHeading() { return hasCompass; }
|
bool hasHeading() { return hasCompass; }
|
||||||
@@ -321,6 +354,12 @@ class Screen : public concurrency::OSThread
|
|||||||
/// Stops showing the boot screen.
|
/// Stops showing the boot screen.
|
||||||
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
|
||||||
|
|
||||||
|
void runNow()
|
||||||
|
{
|
||||||
|
setFastFramerate();
|
||||||
|
enqueueCmd(ScreenCmd{.cmd = Cmd::NOOP});
|
||||||
|
}
|
||||||
|
|
||||||
/// Overrides the default utf8 character conversion, to replace empty space with question marks
|
/// Overrides the default utf8 character conversion, to replace empty space with question marks
|
||||||
static char customFontTableLookup(const uint8_t ch)
|
static char customFontTableLookup(const uint8_t ch)
|
||||||
{
|
{
|
||||||
@@ -602,8 +641,6 @@ class Screen : public concurrency::OSThread
|
|||||||
void handleShowNextFrame();
|
void handleShowNextFrame();
|
||||||
void handleShowPrevFrame();
|
void handleShowPrevFrame();
|
||||||
void handleStartFirmwareUpdateScreen();
|
void handleStartFirmwareUpdateScreen();
|
||||||
void TZPicker();
|
|
||||||
void LoraRegionPicker(uint32_t duration = 30000);
|
|
||||||
|
|
||||||
// Info collected by setFrames method.
|
// Info collected by setFrames method.
|
||||||
// Index location of specific frames.
|
// Index location of specific frames.
|
||||||
@@ -612,7 +649,6 @@ class Screen : public concurrency::OSThread
|
|||||||
struct FramesetInfo {
|
struct FramesetInfo {
|
||||||
struct FramePositions {
|
struct FramePositions {
|
||||||
uint8_t fault = 255;
|
uint8_t fault = 255;
|
||||||
uint8_t textMessage = 255;
|
|
||||||
uint8_t waypoint = 255;
|
uint8_t waypoint = 255;
|
||||||
uint8_t focusedModule = 255;
|
uint8_t focusedModule = 255;
|
||||||
uint8_t log = 255;
|
uint8_t log = 255;
|
||||||
@@ -622,6 +658,12 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t memory = 255;
|
uint8_t memory = 255;
|
||||||
uint8_t gps = 255;
|
uint8_t gps = 255;
|
||||||
uint8_t home = 255;
|
uint8_t home = 255;
|
||||||
|
uint8_t textMessage = 255;
|
||||||
|
uint8_t nodelist = 255;
|
||||||
|
uint8_t nodelist_lastheard = 255;
|
||||||
|
uint8_t nodelist_hopsignal = 255;
|
||||||
|
uint8_t nodelist_distance = 255;
|
||||||
|
uint8_t nodelist_bearings = 255;
|
||||||
uint8_t clock = 255;
|
uint8_t clock = 255;
|
||||||
uint8_t firstFavorite = 255;
|
uint8_t firstFavorite = 255;
|
||||||
uint8_t lastFavorite = 255;
|
uint8_t lastFavorite = 255;
|
||||||
@@ -679,5 +721,6 @@ class Screen : public concurrency::OSThread
|
|||||||
// Extern declarations for function symbols used in UIRenderer
|
// Extern declarations for function symbols used in UIRenderer
|
||||||
extern std::vector<std::string> functionSymbol;
|
extern std::vector<std::string> functionSymbol;
|
||||||
extern std::string functionSymbolString;
|
extern std::string functionSymbolString;
|
||||||
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
#include "graphics/fonts/OLEDDisplayFontsCS.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CROWPANEL_ESP32S3_5_EPAPER
|
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||||
#include "graphics/fonts/EinkDisplayFonts.h"
|
#include "graphics/fonts/EinkDisplayFonts.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -40,6 +40,9 @@
|
|||||||
#ifdef OLED_PL
|
#ifdef OLED_PL
|
||||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
|
||||||
#else
|
#else
|
||||||
|
#ifdef OLED_RU
|
||||||
|
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19
|
||||||
|
#else
|
||||||
#ifdef OLED_UA
|
#ifdef OLED_UA
|
||||||
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
|
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
|
||||||
#else
|
#else
|
||||||
@@ -50,9 +53,13 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
#ifdef OLED_PL
|
#ifdef OLED_PL
|
||||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
|
||||||
#else
|
#else
|
||||||
|
#ifdef OLED_RU
|
||||||
|
#define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28
|
||||||
|
#else
|
||||||
#ifdef OLED_UA
|
#ifdef OLED_UA
|
||||||
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
|
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
|
||||||
#else
|
#else
|
||||||
@@ -63,6 +70,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
|
||||||
@@ -77,7 +85,7 @@
|
|||||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER)
|
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
|
||||||
#undef FONT_SMALL
|
#undef FONT_SMALL
|
||||||
#undef FONT_MEDIUM
|
#undef FONT_MEDIUM
|
||||||
#undef FONT_LARGE
|
#undef FONT_LARGE
|
||||||
|
|||||||
@@ -10,9 +10,22 @@
|
|||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
|
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||||
|
{
|
||||||
|
if (screenwidth > 128) {
|
||||||
|
isHighResolution = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for Heltec Wireless Tracker v1.1
|
||||||
|
if (screenwidth == 160 && screenheight == 80) {
|
||||||
|
isHighResolution = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Shared External State ===
|
// === Shared External State ===
|
||||||
bool hasUnreadMessage = false;
|
bool hasUnreadMessage = false;
|
||||||
bool isMuted = false;
|
bool isMuted = false;
|
||||||
|
bool isHighResolution = false;
|
||||||
|
|
||||||
// === Internal State ===
|
// === Internal State ===
|
||||||
bool isBoltVisibleShared = true;
|
bool isBoltVisibleShared = true;
|
||||||
@@ -40,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
|||||||
// *************************
|
// *************************
|
||||||
// * Common Header Drawing *
|
// * Common Header Drawing *
|
||||||
// *************************
|
// *************************
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr)
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
|
||||||
{
|
{
|
||||||
constexpr int HEADER_OFFSET_Y = 1;
|
constexpr int HEADER_OFFSET_Y = 1;
|
||||||
y += HEADER_OFFSET_Y;
|
y += HEADER_OFFSET_Y;
|
||||||
@@ -56,34 +69,47 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
const int screenW = display->getWidth();
|
const int screenW = display->getWidth();
|
||||||
const int screenH = display->getHeight();
|
const int screenH = display->getHeight();
|
||||||
|
|
||||||
const bool useBigIcons = (screenW > 128);
|
if (!battery_only) {
|
||||||
|
// === Inverted Header Background ===
|
||||||
// === Inverted Header Background ===
|
if (isInverted) {
|
||||||
if (isInverted) {
|
display->setColor(BLACK);
|
||||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(WHITE);
|
||||||
} else {
|
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, 0, screenW, highlightHeight + 3);
|
|
||||||
display->setColor(WHITE);
|
|
||||||
if (screenW > 128) {
|
|
||||||
display->drawLine(0, 20, screenW, 20);
|
|
||||||
} else {
|
} else {
|
||||||
display->drawLine(0, 14, screenW, 14);
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
if (isHighResolution) {
|
||||||
|
display->drawLine(0, 20, screenW, 20);
|
||||||
|
} else {
|
||||||
|
display->drawLine(0, 14, screenW, 14);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// === Screen Title ===
|
// === Screen Title ===
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||||
if (config.display.heading_bold) {
|
if (config.display.heading_bold) {
|
||||||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
// === Battery State ===
|
// === Battery State ===
|
||||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||||
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
|
bool isCharging = powerStatus->getIsCharging();
|
||||||
|
bool usbPowered = powerStatus->getHasUSB();
|
||||||
|
|
||||||
|
if (chargePercent >= 100) {
|
||||||
|
isCharging = false;
|
||||||
|
}
|
||||||
|
if (chargePercent == 101) {
|
||||||
|
usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable
|
||||||
|
// plugged in
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
@@ -93,53 +119,66 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool useHorizontalBattery = (screenW > 128 && screenW >= screenH);
|
bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
|
||||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
|
||||||
|
int batteryX = 1;
|
||||||
|
int batteryY = HEADER_OFFSET_Y + 1;
|
||||||
|
|
||||||
// === Battery Icons ===
|
// === Battery Icons ===
|
||||||
if (useHorizontalBattery) {
|
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||||||
int batteryX = 2;
|
batteryX += 1;
|
||||||
int batteryY = HEADER_OFFSET_Y + 2;
|
batteryY += 2;
|
||||||
display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h);
|
if (isHighResolution) {
|
||||||
if (isCharging && isBoltVisibleShared)
|
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
||||||
display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h);
|
batteryX += 20; // Icon + 1 pixel
|
||||||
else {
|
} else {
|
||||||
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
|
display->drawXbm(batteryX, batteryY, 10, 8, imgUSB);
|
||||||
int fillWidth = 24 * chargePercent / 100;
|
batteryX += 11; // Icon + 1 pixel
|
||||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int batteryX = 1;
|
if (useHorizontalBattery) {
|
||||||
int batteryY = HEADER_OFFSET_Y + 1;
|
batteryX += 1;
|
||||||
|
batteryY += 2;
|
||||||
|
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||||||
|
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||||||
|
if (isCharging && isBoltVisibleShared)
|
||||||
|
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||||||
|
else {
|
||||||
|
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||||||
|
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||||
|
int fillWidth = 14 * chargePercent / 100;
|
||||||
|
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||||
|
}
|
||||||
|
batteryX += 18; // Icon + 2 pixels
|
||||||
|
} else {
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
batteryY += 2;
|
batteryY += 2;
|
||||||
#endif
|
#endif
|
||||||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||||
if (isCharging && isBoltVisibleShared)
|
if (isCharging && isBoltVisibleShared)
|
||||||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||||
else {
|
else {
|
||||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||||
int fillHeight = 8 * chargePercent / 100;
|
int fillHeight = 8 * chargePercent / 100;
|
||||||
int fillY = batteryY - fillHeight;
|
int fillY = batteryY - fillHeight;
|
||||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||||
|
}
|
||||||
|
batteryX += 9; // Icon + 2 pixels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Battery % Display ===
|
if (chargePercent != 101) {
|
||||||
char chargeStr[4];
|
// === Battery % Display ===
|
||||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
char chargeStr[4];
|
||||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||||
const int batteryOffset = useHorizontalBattery ? 28 : 6;
|
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||||
#ifdef USE_EINK
|
display->drawString(batteryX, textY, chargeStr);
|
||||||
const int percentX = x + xOffset + batteryOffset - 2;
|
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||||||
#else
|
if (isBold) {
|
||||||
const int percentX = x + xOffset + batteryOffset;
|
display->drawString(batteryX + 1, textY, chargeStr);
|
||||||
#endif
|
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||||||
display->drawString(percentX, textY, chargeStr);
|
}
|
||||||
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
|
||||||
if (isBold) {
|
|
||||||
display->drawString(percentX + 1, textY, chargeStr);
|
|
||||||
display->drawString(percentX + chargeNumWidth, textY, "%");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Time and Right-aligned Icons ===
|
// === Time and Right-aligned Icons ===
|
||||||
@@ -148,7 +187,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||||
|
|
||||||
if (rtc_sec > 0) {
|
if (rtc_sec > 0 && !battery_only) {
|
||||||
// === Build Time String ===
|
// === Build Time String ===
|
||||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
int hour = hms / SEC_PER_HOUR;
|
int hour = hms / SEC_PER_HOUR;
|
||||||
@@ -164,10 +203,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeStrWidth = display->getStringWidth(timeStr);
|
timeStrWidth = display->getStringWidth(timeStr);
|
||||||
timeX = screenW - xOffset - timeStrWidth + 4;
|
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||||
|
|
||||||
// === Show Mail or Mute Icon to the Left of Time ===
|
// === Show Mail or Mute Icon to the Left of Time ===
|
||||||
int iconRightEdge = timeX - 1;
|
int iconRightEdge = timeX - 2;
|
||||||
|
|
||||||
bool showMail = false;
|
bool showMail = false;
|
||||||
|
|
||||||
@@ -217,7 +256,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (isMuted) {
|
} else if (isMuted) {
|
||||||
if (useBigIcons) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
|
||||||
@@ -259,6 +298,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
|
|
||||||
bool showMail = false;
|
bool showMail = false;
|
||||||
|
|
||||||
|
#ifndef USE_EINK
|
||||||
if (hasUnreadMessage) {
|
if (hasUnreadMessage) {
|
||||||
if (now - lastMailBlink > 500) {
|
if (now - lastMailBlink > 500) {
|
||||||
isMailIconVisible = !isMailIconVisible;
|
isMailIconVisible = !isMailIconVisible;
|
||||||
@@ -266,6 +306,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
showMail = isMailIconVisible;
|
showMail = isMailIconVisible;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
showMail = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (showMail) {
|
if (showMail) {
|
||||||
if (useHorizontalBattery) {
|
if (useHorizontalBattery) {
|
||||||
@@ -281,7 +326,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (isMuted) {
|
} else if (isMuted) {
|
||||||
if (useBigIcons) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||||
@@ -300,7 +345,7 @@ const int *getTextPositions(OLEDDisplay *display)
|
|||||||
{
|
{
|
||||||
static int textPositions[7]; // Static array that persists beyond function scope
|
static int textPositions[7]; // Static array that persists beyond function scope
|
||||||
|
|
||||||
if (display->getHeight() > 64) {
|
if (isHighResolution) {
|
||||||
textPositions[0] = textZeroLine;
|
textPositions[0] = textZeroLine;
|
||||||
textPositions[1] = textFirstLine_medium;
|
textPositions[1] = textFirstLine_medium;
|
||||||
textPositions[2] = textSecondLine_medium;
|
textPositions[2] = textSecondLine_medium;
|
||||||
@@ -320,4 +365,30 @@ const int *getTextPositions(OLEDDisplay *display)
|
|||||||
return textPositions;
|
return textPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAllowedPunctuation(char c)
|
||||||
|
{
|
||||||
|
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||||
|
return allowed.find(c) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sanitizeString(const std::string &input)
|
||||||
|
{
|
||||||
|
std::string output;
|
||||||
|
bool inReplacement = false;
|
||||||
|
|
||||||
|
for (char c : input) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c)) || isAllowedPunctuation(c)) {
|
||||||
|
output += c;
|
||||||
|
inReplacement = false;
|
||||||
|
} else {
|
||||||
|
if (!inReplacement) {
|
||||||
|
output += 0xbf; // ISO-8859-1 for inverted question mark
|
||||||
|
inReplacement = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
@@ -41,13 +42,19 @@ namespace graphics
|
|||||||
// Shared state (declare inside namespace)
|
// Shared state (declare inside namespace)
|
||||||
extern bool hasUnreadMessage;
|
extern bool hasUnreadMessage;
|
||||||
extern bool isMuted;
|
extern bool isMuted;
|
||||||
|
extern bool isHighResolution;
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||||
|
|
||||||
// Rounded highlight (used for inverted headers)
|
// Rounded highlight (used for inverted headers)
|
||||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||||
|
|
||||||
// Shared battery/time/mail header
|
// Shared battery/time/mail header
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "");
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
|
||||||
|
|
||||||
const int *getTextPositions(OLEDDisplay *display);
|
const int *getTextPositions(OLEDDisplay *display);
|
||||||
|
|
||||||
|
bool isAllowedPunctuation(char c);
|
||||||
|
|
||||||
|
std::string sanitizeString(const std::string &input);
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -14,8 +15,10 @@
|
|||||||
extern SX1509 gpioExtender;
|
extern SX1509 gpioExtender;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef TFT_MESH
|
#ifdef TFT_MESH_OVERRIDE
|
||||||
#define TFT_MESH COLOR565(0x67, 0xEA, 0x94)
|
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
|
||||||
|
#else
|
||||||
|
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(ST7735S)
|
#if defined(ST7735S)
|
||||||
@@ -664,15 +667,19 @@ static LGFX *tft = nullptr;
|
|||||||
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
|
||||||
#elif ARCH_PORTDUINO
|
#elif ARCH_PORTDUINO
|
||||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||||
|
#if defined(LGFX_SDL)
|
||||||
|
#include <lgfx/v1/platforms/sdl/Panel_sdl.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
class LGFX : public lgfx::LGFX_Device
|
class LGFX : public lgfx::LGFX_Device
|
||||||
{
|
{
|
||||||
lgfx::Panel_Device *_panel_instance;
|
|
||||||
lgfx::Bus_SPI _bus_instance;
|
lgfx::Bus_SPI _bus_instance;
|
||||||
|
|
||||||
lgfx::ITouch *_touch_instance;
|
lgfx::ITouch *_touch_instance;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
lgfx::Panel_Device *_panel_instance;
|
||||||
|
|
||||||
LGFX(void)
|
LGFX(void)
|
||||||
{
|
{
|
||||||
if (settingsMap[displayPanel] == st7789)
|
if (settingsMap[displayPanel] == st7789)
|
||||||
@@ -691,6 +698,11 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
_panel_instance = new lgfx::Panel_ILI9488;
|
_panel_instance = new lgfx::Panel_ILI9488;
|
||||||
else if (settingsMap[displayPanel] == hx8357d)
|
else if (settingsMap[displayPanel] == hx8357d)
|
||||||
_panel_instance = new lgfx::Panel_HX8357D;
|
_panel_instance = new lgfx::Panel_HX8357D;
|
||||||
|
#if defined(LGFX_SDL)
|
||||||
|
else if (settingsMap[displayPanel] == x11) {
|
||||||
|
_panel_instance = new lgfx::Panel_sdl;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
else {
|
else {
|
||||||
_panel_instance = new lgfx::Panel_NULL;
|
_panel_instance = new lgfx::Panel_NULL;
|
||||||
LOG_ERROR("Unknown display panel configured!");
|
LOG_ERROR("Unknown display panel configured!");
|
||||||
@@ -751,7 +763,13 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
_touch_instance->config(touch_cfg);
|
_touch_instance->config(touch_cfg);
|
||||||
_panel_instance->setTouch(_touch_instance);
|
_panel_instance->setTouch(_touch_instance);
|
||||||
}
|
}
|
||||||
|
#if defined(LGFX_SDL)
|
||||||
|
if (settingsMap[displayPanel] == x11) {
|
||||||
|
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
|
||||||
|
sdl_panel_->setup();
|
||||||
|
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
setPanel(_panel_instance); // Sets the panel to use.
|
setPanel(_panel_instance); // Sets the panel to use.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -846,9 +864,29 @@ static LGFX *tft = nullptr;
|
|||||||
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
|
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
|
||||||
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
|
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
|
||||||
|
|
||||||
|
class PanelInit_ST7701 : public lgfx::Panel_ST7701
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
const uint8_t *getInitCommands(uint8_t listno) const override
|
||||||
|
{
|
||||||
|
// 180 degree hw rotation: vertical flip, horizontal flip
|
||||||
|
static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip
|
||||||
|
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
|
||||||
|
0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip)
|
||||||
|
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
|
||||||
|
0xFF, 0xFF};
|
||||||
|
switch (listno) {
|
||||||
|
case 1:
|
||||||
|
return list1;
|
||||||
|
default:
|
||||||
|
return lgfx::Panel_ST7701::getInitCommands(listno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class LGFX : public lgfx::LGFX_Device
|
class LGFX : public lgfx::LGFX_Device
|
||||||
{
|
{
|
||||||
lgfx::Panel_ST7701 _panel_instance;
|
PanelInit_ST7701 _panel_instance;
|
||||||
lgfx::Bus_RGB _bus_instance;
|
lgfx::Bus_RGB _bus_instance;
|
||||||
lgfx::Light_PWM _light_instance;
|
lgfx::Light_PWM _light_instance;
|
||||||
lgfx::Touch_FT5x06 _touch_instance;
|
lgfx::Touch_FT5x06 _touch_instance;
|
||||||
@@ -1037,6 +1075,49 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TFTDisplay::sdlLoop()
|
||||||
|
{
|
||||||
|
#if defined(LGFX_SDL)
|
||||||
|
static int lastPressed = 0;
|
||||||
|
static int shuttingDown = false;
|
||||||
|
if (settingsMap[displayPanel] == x11) {
|
||||||
|
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
||||||
|
if (sdl_panel_->loop() && !shuttingDown) {
|
||||||
|
LOG_WARN("Window Closed!");
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// debounce
|
||||||
|
if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
|
||||||
|
return;
|
||||||
|
if (!lgfx::v1::gpio_in(37)) {
|
||||||
|
lastPressed = 37;
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
} else if (!lgfx::v1::gpio_in(36)) {
|
||||||
|
lastPressed = 36;
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
} else if (!lgfx::v1::gpio_in(38)) {
|
||||||
|
lastPressed = 38;
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
} else if (!lgfx::v1::gpio_in(39)) {
|
||||||
|
lastPressed = 39;
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
} else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) {
|
||||||
|
lastPressed = SDL_SCANCODE_KP_ENTER;
|
||||||
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
|
inputBroker->injectInputEvent(&event);
|
||||||
|
} else {
|
||||||
|
lastPressed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Send a command to the display (low level function)
|
// Send a command to the display (low level function)
|
||||||
void TFTDisplay::sendCommand(uint8_t com)
|
void TFTDisplay::sendCommand(uint8_t com)
|
||||||
{
|
{
|
||||||
@@ -1181,9 +1262,9 @@ bool TFTDisplay::connect()
|
|||||||
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
|
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
|
||||||
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
|
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
|
||||||
tft->setRotation(1); // T-Deck has the TFT in landscape
|
tft->setRotation(1); // T-Deck has the TFT in landscape
|
||||||
#elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
#elif defined(T_WATCH_S3)
|
||||||
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
tft->setRotation(2); // T-Watch S3 left-handed orientation
|
||||||
#elif ARCH_PORTDUINO
|
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR)
|
||||||
tft->setRotation(0); // use config.yaml to set rotation
|
tft->setRotation(0); // use config.yaml to set rotation
|
||||||
#else
|
#else
|
||||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class TFTDisplay : public OLEDDisplay
|
|||||||
// Write the buffer to the display memory
|
// Write the buffer to the display memory
|
||||||
virtual void display() override { display(false); };
|
virtual void display() override { display(false); };
|
||||||
virtual void display(bool fromBlank);
|
virtual void display(bool fromBlank);
|
||||||
|
void sdlLoop();
|
||||||
|
|
||||||
// Turn the display upside down
|
// Turn the display upside down
|
||||||
virtual void flipScreenVertically();
|
virtual void flipScreenVertically();
|
||||||
|
|||||||
738
src/graphics/VirtualKeyboard.cpp
Normal file
738
src/graphics/VirtualKeyboard.cpp
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
#include "VirtualKeyboard.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
|
||||||
|
{
|
||||||
|
initializeKeyboard();
|
||||||
|
// Set cursor to H(2, 5)
|
||||||
|
cursorRow = 2;
|
||||||
|
cursorCol = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualKeyboard::~VirtualKeyboard() {}
|
||||||
|
|
||||||
|
void VirtualKeyboard::initializeKeyboard()
|
||||||
|
{
|
||||||
|
// New 4 row, 11 column keyboard layout:
|
||||||
|
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
|
||||||
|
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
|
||||||
|
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
|
||||||
|
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
|
||||||
|
|
||||||
|
// Derive layout dimensions and assert they match the configured keyboard grid
|
||||||
|
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
|
||||||
|
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
|
||||||
|
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
|
||||||
|
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
|
||||||
|
|
||||||
|
// Initialize all keys to empty first
|
||||||
|
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||||
|
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||||
|
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill keyboard from the 2D layout
|
||||||
|
for (int row = 0; row < LAYOUT_ROWS; row++) {
|
||||||
|
for (int col = 0; col < LAYOUT_COLS; col++) {
|
||||||
|
char ch = LAYOUT[row][col];
|
||||||
|
// No empty slots in the simplified layout
|
||||||
|
|
||||||
|
VirtualKeyType type = VK_CHAR;
|
||||||
|
if (ch == '\b') {
|
||||||
|
type = VK_BACKSPACE;
|
||||||
|
} else if (ch == '\n') {
|
||||||
|
type = VK_ENTER;
|
||||||
|
} else if (ch == '\x1b') { // ESC
|
||||||
|
type = VK_ESC;
|
||||||
|
} else if (ch == ' ') {
|
||||||
|
type = VK_SPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make action keys wider to fit text while keeping the last column aligned
|
||||||
|
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
|
||||||
|
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||||
|
{
|
||||||
|
// Repeat ticking is driven by NotificationRenderer once per frame
|
||||||
|
// Base styles
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
// Screen geometry
|
||||||
|
const int screenW = display->getWidth();
|
||||||
|
const int screenH = display->getHeight();
|
||||||
|
|
||||||
|
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
|
||||||
|
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
|
||||||
|
const bool isWide = screenW >= 200;
|
||||||
|
|
||||||
|
// Determine last-column label max width
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
const int wENTER = display->getStringWidth("ENTER");
|
||||||
|
int lastColLabelW = wENTER; // ENTER is usually the widest
|
||||||
|
// Smaller padding on very small screens to avoid excessive whitespace
|
||||||
|
const int lastColPad = (screenW <= 128 ? 2 : 6);
|
||||||
|
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
|
||||||
|
|
||||||
|
// Always reserve width for the rightmost text column to avoid overlap on small screens
|
||||||
|
int cellW = 0;
|
||||||
|
int leftoverW = 0;
|
||||||
|
{
|
||||||
|
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
|
||||||
|
int usableW = screenW - reservedLastColW;
|
||||||
|
if (usableW < leftCols) {
|
||||||
|
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
|
||||||
|
usableW = leftCols;
|
||||||
|
}
|
||||||
|
cellW = usableW / leftCols;
|
||||||
|
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic key geometry
|
||||||
|
int cellH = KEY_HEIGHT;
|
||||||
|
int keyboardStartY = 0;
|
||||||
|
if (screenH <= 64) {
|
||||||
|
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
|
||||||
|
const int gapBelowHeader = 0;
|
||||||
|
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
|
||||||
|
const int gapAboveKeyboard = 0;
|
||||||
|
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
|
||||||
|
if (keyboardStartY < 0)
|
||||||
|
keyboardStartY = 0;
|
||||||
|
if (keyboardStartY > screenH)
|
||||||
|
keyboardStartY = screenH;
|
||||||
|
int keyboardHeight = screenH - keyboardStartY;
|
||||||
|
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
|
||||||
|
} else if (isWide) {
|
||||||
|
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
|
||||||
|
cellH = std::max((int)KEY_HEIGHT, cellW);
|
||||||
|
|
||||||
|
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
|
||||||
|
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
|
||||||
|
const int headerToBoxGap = 1;
|
||||||
|
const int gapAboveKb = 1;
|
||||||
|
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
|
||||||
|
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
|
||||||
|
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
|
||||||
|
if (maxCellHAllowed < (int)KEY_HEIGHT)
|
||||||
|
maxCellHAllowed = KEY_HEIGHT;
|
||||||
|
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
|
||||||
|
cellH = maxCellHAllowed;
|
||||||
|
}
|
||||||
|
// Keyboard placement from bottom for wide screens
|
||||||
|
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||||
|
keyboardStartY = screenH - keyboardHeight;
|
||||||
|
if (keyboardStartY < 0)
|
||||||
|
keyboardStartY = 0;
|
||||||
|
} else {
|
||||||
|
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
|
||||||
|
cellH = KEY_HEIGHT;
|
||||||
|
int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||||
|
keyboardStartY = screenH - keyboardHeight;
|
||||||
|
if (keyboardStartY < 0)
|
||||||
|
keyboardStartY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw input area above keyboard
|
||||||
|
drawInputArea(display, offsetX, offsetY, keyboardStartY);
|
||||||
|
|
||||||
|
// Precompute per-column x and width with leftover distributed over left columns for even spacing
|
||||||
|
int colX[KEYBOARD_COLS];
|
||||||
|
int colW[KEYBOARD_COLS];
|
||||||
|
int runningX = offsetX;
|
||||||
|
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
|
||||||
|
int wcol = cellW + (col < leftoverW ? 1 : 0);
|
||||||
|
colX[col] = runningX;
|
||||||
|
colW[col] = wcol;
|
||||||
|
runningX += wcol;
|
||||||
|
}
|
||||||
|
// Last column
|
||||||
|
colX[KEYBOARD_COLS - 1] = runningX;
|
||||||
|
colW[KEYBOARD_COLS - 1] = reservedLastColW;
|
||||||
|
|
||||||
|
// Draw keyboard grid
|
||||||
|
for (int row = 0; row < KEYBOARD_ROWS; row++) {
|
||||||
|
for (int col = 0; col < KEYBOARD_COLS; col++) {
|
||||||
|
const VirtualKey &k = keyboard[row][col];
|
||||||
|
if (k.character != 0 || k.type != VK_CHAR) {
|
||||||
|
const bool isLastCol = (col == KEYBOARD_COLS - 1);
|
||||||
|
int x = colX[col];
|
||||||
|
int w = colW[col];
|
||||||
|
int y = offsetY + keyboardStartY + row * cellH;
|
||||||
|
int h = cellH;
|
||||||
|
bool selected = (row == cursorRow && col == cursorCol);
|
||||||
|
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
|
||||||
|
{
|
||||||
|
display->setColor(WHITE);
|
||||||
|
|
||||||
|
const int screenWidth = display->getWidth();
|
||||||
|
const int screenHeight = display->getHeight();
|
||||||
|
// Use the standard small font metrics for input box sizing (restore original size)
|
||||||
|
const int inputLineH = FONT_HEIGHT_SMALL;
|
||||||
|
|
||||||
|
// Header uses the standard small (which may be larger on big screens)
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int headerHeight = 0;
|
||||||
|
if (!headerText.empty()) {
|
||||||
|
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
|
||||||
|
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||||
|
if (screenHeight <= 64) {
|
||||||
|
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
|
||||||
|
} else {
|
||||||
|
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int boxX = offsetX;
|
||||||
|
const int boxWidth = screenWidth;
|
||||||
|
int boxY;
|
||||||
|
int boxHeight;
|
||||||
|
if (screenHeight <= 64) {
|
||||||
|
const int gapBelowHeader = 0;
|
||||||
|
const int fixedBoxHeight = inputLineH;
|
||||||
|
const int gapAboveKeyboard = 0;
|
||||||
|
boxY = offsetY + headerHeight + gapBelowHeader;
|
||||||
|
boxHeight = fixedBoxHeight;
|
||||||
|
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
|
||||||
|
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
|
||||||
|
boxHeight = std::max(1, fixedBoxHeight - over);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const int gapBelowHeader = 1;
|
||||||
|
int gapAboveKeyboard = 1;
|
||||||
|
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
|
||||||
|
const int minBoxHeight = inputLineH + 2;
|
||||||
|
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
|
||||||
|
if (availableH < minBoxHeight)
|
||||||
|
availableH = minBoxHeight;
|
||||||
|
boxY = tmpBoxY;
|
||||||
|
boxHeight = availableH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw box border
|
||||||
|
display->drawRect(boxX, boxY, boxWidth, boxHeight);
|
||||||
|
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
|
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
|
||||||
|
const int textX = boxX + 2;
|
||||||
|
const int maxTextWidth = boxWidth - 4;
|
||||||
|
const int maxLines = (boxHeight - 2) / inputLineH;
|
||||||
|
if (maxLines >= 2) {
|
||||||
|
// Inner bounds for caret clamping
|
||||||
|
const int innerLeft = boxX + 1;
|
||||||
|
const int innerRight = boxX + boxWidth - 2;
|
||||||
|
const int innerTop = boxY + 1;
|
||||||
|
const int innerBottom = boxY + boxHeight - 2;
|
||||||
|
|
||||||
|
// Wrap text greedily into lines that fit maxTextWidth
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
{
|
||||||
|
std::string remaining = inputText;
|
||||||
|
while (!remaining.empty()) {
|
||||||
|
int bestLen = 0;
|
||||||
|
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||||
|
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||||
|
if (w <= maxTextWidth)
|
||||||
|
bestLen = len;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (bestLen == 0) {
|
||||||
|
// At least show one character to make progress
|
||||||
|
bestLen = 1;
|
||||||
|
}
|
||||||
|
lines.emplace_back(remaining.substr(0, bestLen));
|
||||||
|
remaining.erase(0, bestLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool scrolledUp = ((int)lines.size() > maxLines);
|
||||||
|
int caretX = textX;
|
||||||
|
int caretY = innerTop;
|
||||||
|
|
||||||
|
// Leave a small top gap to render '...' without replacing the first line
|
||||||
|
const int topInset = 2;
|
||||||
|
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
|
||||||
|
int lineY = innerTop + topInset;
|
||||||
|
|
||||||
|
if (scrolledUp) {
|
||||||
|
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
|
||||||
|
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
|
||||||
|
const int firstLineTop = lineY; // baseline top for the first visible line
|
||||||
|
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
|
||||||
|
const int centerX = boxX + boxWidth / 2;
|
||||||
|
const int dotSpacing = 3; // px between dots
|
||||||
|
const int dotSize = 1; // small square dot
|
||||||
|
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
|
||||||
|
display->fillRect(centerX, gapMidY, dotSize, dotSize);
|
||||||
|
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many lines fit with our top inset and tighter step
|
||||||
|
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
|
||||||
|
const int linesToShow = std::min((int)lines.size(), linesCapacity);
|
||||||
|
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < linesToShow; ++i) {
|
||||||
|
const std::string &chunk = lines[startIndex + i];
|
||||||
|
display->drawString(textX, lineY, chunk.c_str());
|
||||||
|
caretX = textX + display->getStringWidth(chunk.c_str());
|
||||||
|
caretY = lineY;
|
||||||
|
lineY += lineStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw caret at end of the last visible line
|
||||||
|
int caretPadY = 2;
|
||||||
|
if (boxHeight >= inputLineH + 4)
|
||||||
|
caretPadY = 3;
|
||||||
|
int cursorTop = caretY + caretPadY;
|
||||||
|
// Use lineStep so caret height matches the row spacing
|
||||||
|
int cursorH = lineStep - caretPadY * 2;
|
||||||
|
if (cursorH < 1)
|
||||||
|
cursorH = 1;
|
||||||
|
// Clamp vertical bounds to stay inside the inner rect
|
||||||
|
if (cursorTop < innerTop)
|
||||||
|
cursorTop = innerTop;
|
||||||
|
if (cursorTop + cursorH - 1 > innerBottom)
|
||||||
|
cursorH = innerBottom - cursorTop + 1;
|
||||||
|
if (cursorH < 1)
|
||||||
|
cursorH = 1;
|
||||||
|
// Only draw if cursor is inside inner bounds
|
||||||
|
if (caretX >= innerLeft && caretX <= innerRight) {
|
||||||
|
display->drawVerticalLine(caretX, cursorTop, cursorH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::string displayText = inputText;
|
||||||
|
int textW = display->getStringWidth(displayText.c_str());
|
||||||
|
std::string scrolled = displayText;
|
||||||
|
if (textW > maxTextWidth) {
|
||||||
|
// Trim from the left until it fits
|
||||||
|
while (textW > maxTextWidth && !scrolled.empty()) {
|
||||||
|
scrolled.erase(0, 1);
|
||||||
|
textW = display->getStringWidth(scrolled.c_str());
|
||||||
|
}
|
||||||
|
// Add leading ellipsis and ensure it still fits
|
||||||
|
if (scrolled != displayText) {
|
||||||
|
scrolled = "..." + scrolled;
|
||||||
|
textW = display->getStringWidth(scrolled.c_str());
|
||||||
|
// If adding ellipsis causes overflow, trim more after the ellipsis
|
||||||
|
while (textW > maxTextWidth && scrolled.size() > 3) {
|
||||||
|
scrolled.erase(3, 1); // remove chars after the ellipsis
|
||||||
|
textW = display->getStringWidth(scrolled.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep textW in sync with what we draw
|
||||||
|
textW = display->getStringWidth(scrolled.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int textY;
|
||||||
|
if (screenHeight <= 64) {
|
||||||
|
textY = boxY + (boxHeight - inputLineH) / 2;
|
||||||
|
} else {
|
||||||
|
const int innerLeft = boxX + 1;
|
||||||
|
const int innerRight = boxX + boxWidth - 2;
|
||||||
|
const int innerTop = boxY + 1;
|
||||||
|
const int innerBottom = boxY + boxHeight - 2;
|
||||||
|
|
||||||
|
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
|
||||||
|
int innerH = innerBottom - innerTop + 1;
|
||||||
|
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
|
||||||
|
// Clamp fully inside the inner rect
|
||||||
|
if (textY < innerTop)
|
||||||
|
textY = innerTop;
|
||||||
|
int maxTop = innerBottom - inputLineH + 1;
|
||||||
|
if (textY > maxTop)
|
||||||
|
textY = maxTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scrolled.empty()) {
|
||||||
|
display->drawString(textX, textY, scrolled.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int cursorX = textX + textW;
|
||||||
|
if (screenHeight > 64) {
|
||||||
|
const int innerRight = boxX + boxWidth - 2;
|
||||||
|
if (cursorX > innerRight)
|
||||||
|
cursorX = innerRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cursorTop, cursorH;
|
||||||
|
if (screenHeight <= 64) {
|
||||||
|
cursorH = 10;
|
||||||
|
cursorTop = boxY + (boxHeight - cursorH) / 2;
|
||||||
|
} else {
|
||||||
|
const int innerLeft = boxX + 1;
|
||||||
|
const int innerRight = boxX + boxWidth - 2;
|
||||||
|
const int innerTop = boxY + 1;
|
||||||
|
const int innerBottom = boxY + boxHeight - 2;
|
||||||
|
|
||||||
|
cursorTop = boxY + 2;
|
||||||
|
cursorH = boxHeight - 4;
|
||||||
|
if (cursorH < 1)
|
||||||
|
cursorH = 1;
|
||||||
|
if (cursorTop < innerTop)
|
||||||
|
cursorTop = innerTop;
|
||||||
|
if (cursorTop + cursorH - 1 > innerBottom)
|
||||||
|
cursorH = innerBottom - cursorTop + 1;
|
||||||
|
if (cursorH < 1)
|
||||||
|
cursorH = 1;
|
||||||
|
|
||||||
|
if (cursorX < innerLeft || cursorX > innerRight)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
display->drawVerticalLine(cursorX, cursorTop, cursorH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
|
||||||
|
uint8_t height, bool isLastCol)
|
||||||
|
{
|
||||||
|
// Draw key content
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
const int fontH = FONT_HEIGHT_SMALL;
|
||||||
|
// Build label and metrics first
|
||||||
|
std::string keyText;
|
||||||
|
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
|
||||||
|
// Keep literal text labels for the action keys on the rightmost column
|
||||||
|
keyText = (key.type == VK_BACKSPACE) ? "BACK"
|
||||||
|
: (key.type == VK_ENTER) ? "ENTER"
|
||||||
|
: (key.type == VK_SPACE) ? "SPACE"
|
||||||
|
: (key.type == VK_ESC) ? "ESC"
|
||||||
|
: "";
|
||||||
|
} else {
|
||||||
|
char c = getCharForKey(key, false);
|
||||||
|
if (c >= 'a' && c <= 'z') {
|
||||||
|
c = c - 'a' + 'A';
|
||||||
|
}
|
||||||
|
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
int textWidth = display->getStringWidth(keyText.c_str());
|
||||||
|
// Label alignment
|
||||||
|
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
|
||||||
|
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
|
||||||
|
int textX;
|
||||||
|
if (isLastCol) {
|
||||||
|
const int rightPad = 1;
|
||||||
|
textX = x + width - textWidth - rightPad;
|
||||||
|
if (textX < x)
|
||||||
|
textX = x; // guard
|
||||||
|
} else {
|
||||||
|
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
|
||||||
|
textX = x + (width - textWidth + 1) / 2;
|
||||||
|
} else {
|
||||||
|
textX = x + (width - textWidth) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int contentTop = y;
|
||||||
|
int contentH = height;
|
||||||
|
if (selected) {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
|
||||||
|
|
||||||
|
if (display->getHeight() <= 64 && !isAction) {
|
||||||
|
display->fillRect(x, y, width, height);
|
||||||
|
} else if (isAction) {
|
||||||
|
const int padX = 1;
|
||||||
|
const int padY = 2;
|
||||||
|
int hlW = textWidth + padX * 2;
|
||||||
|
int hlX = textX - padX;
|
||||||
|
|
||||||
|
if (hlX < x) {
|
||||||
|
hlW -= (x - hlX);
|
||||||
|
hlX = x;
|
||||||
|
}
|
||||||
|
int maxW = (x + width) - hlX;
|
||||||
|
if (hlW > maxW)
|
||||||
|
hlW = maxW;
|
||||||
|
if (hlW < 1)
|
||||||
|
hlW = 1;
|
||||||
|
|
||||||
|
int hlH = std::min(fontH + padY * 2, (int)height);
|
||||||
|
int hlY = y + (height - hlH) / 2;
|
||||||
|
display->fillRect(hlX, hlY, hlW, hlH);
|
||||||
|
contentTop = hlY;
|
||||||
|
contentH = hlH;
|
||||||
|
} else {
|
||||||
|
display->fillRect(x, y, width, height);
|
||||||
|
}
|
||||||
|
display->setColor(BLACK);
|
||||||
|
} else {
|
||||||
|
display->setColor(WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
int centeredTextY;
|
||||||
|
if (display->getHeight() <= 64) {
|
||||||
|
centeredTextY = y + (height - fontH) / 2;
|
||||||
|
} else {
|
||||||
|
centeredTextY = contentTop + (contentH - fontH) / 2;
|
||||||
|
}
|
||||||
|
if (display->getHeight() > 64) {
|
||||||
|
if (centeredTextY < contentTop)
|
||||||
|
centeredTextY = contentTop;
|
||||||
|
if (centeredTextY + fontH > contentTop + contentH)
|
||||||
|
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display->getHeight() <= 64 && keyText.size() == 1) {
|
||||||
|
char ch = keyText[0];
|
||||||
|
if (ch == '.' || ch == ',' || ch == ';') {
|
||||||
|
centeredTextY -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display->drawString(textX, centeredTextY, keyText.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||||
|
{
|
||||||
|
if (key.type != VK_CHAR) {
|
||||||
|
return key.character;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = key.character;
|
||||||
|
|
||||||
|
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
|
||||||
|
if (isLongPress && c >= 'a' && c <= 'z') {
|
||||||
|
c = (char)(c - 'a' + 'A');
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
|
||||||
|
{
|
||||||
|
resetTimeout();
|
||||||
|
// wrap around rows and cols in the 4x11 grid
|
||||||
|
int r = (int)cursorRow + dRow;
|
||||||
|
int c = (int)cursorCol + dCol;
|
||||||
|
if (r < 0)
|
||||||
|
r = KEYBOARD_ROWS - 1;
|
||||||
|
else if (r >= KEYBOARD_ROWS)
|
||||||
|
r = 0;
|
||||||
|
if (c < 0)
|
||||||
|
c = KEYBOARD_COLS - 1;
|
||||||
|
else if (c >= KEYBOARD_COLS)
|
||||||
|
c = 0;
|
||||||
|
cursorRow = (uint8_t)r;
|
||||||
|
cursorCol = (uint8_t)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::moveCursorUp()
|
||||||
|
{
|
||||||
|
moveCursorDelta(-1, 0);
|
||||||
|
}
|
||||||
|
void VirtualKeyboard::moveCursorDown()
|
||||||
|
{
|
||||||
|
moveCursorDelta(1, 0);
|
||||||
|
}
|
||||||
|
void VirtualKeyboard::moveCursorLeft()
|
||||||
|
{
|
||||||
|
resetTimeout();
|
||||||
|
|
||||||
|
if (cursorCol > 0) {
|
||||||
|
cursorCol--;
|
||||||
|
} else {
|
||||||
|
if (cursorRow > 0) {
|
||||||
|
cursorRow--;
|
||||||
|
cursorCol = KEYBOARD_COLS - 1;
|
||||||
|
} else {
|
||||||
|
cursorRow = KEYBOARD_ROWS - 1;
|
||||||
|
cursorCol = KEYBOARD_COLS - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void VirtualKeyboard::moveCursorRight()
|
||||||
|
{
|
||||||
|
resetTimeout();
|
||||||
|
|
||||||
|
if (cursorCol < KEYBOARD_COLS - 1) {
|
||||||
|
cursorCol++;
|
||||||
|
} else {
|
||||||
|
if (cursorRow < KEYBOARD_ROWS - 1) {
|
||||||
|
cursorRow++;
|
||||||
|
cursorCol = 0;
|
||||||
|
} else {
|
||||||
|
cursorRow = 0;
|
||||||
|
cursorCol = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::handlePress()
|
||||||
|
{
|
||||||
|
resetTimeout(); // Reset timeout on any input activity
|
||||||
|
|
||||||
|
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||||
|
|
||||||
|
// Don't handle press if the key is empty (but allow special keys)
|
||||||
|
if (key.character == 0 && key.type == VK_CHAR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For character keys, insert lowercase character
|
||||||
|
if (key.type == VK_CHAR) {
|
||||||
|
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-character keys immediately
|
||||||
|
switch (key.type) {
|
||||||
|
case VK_BACKSPACE:
|
||||||
|
deleteCharacter();
|
||||||
|
break;
|
||||||
|
case VK_ENTER:
|
||||||
|
submitText();
|
||||||
|
break;
|
||||||
|
case VK_SPACE:
|
||||||
|
insertCharacter(' ');
|
||||||
|
break;
|
||||||
|
case VK_ESC:
|
||||||
|
if (onTextEntered) {
|
||||||
|
std::function<void(const std::string &)> callback = onTextEntered;
|
||||||
|
onTextEntered = nullptr;
|
||||||
|
inputText = "";
|
||||||
|
callback("");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::handleLongPress()
|
||||||
|
{
|
||||||
|
resetTimeout(); // Reset timeout on any input activity
|
||||||
|
|
||||||
|
const VirtualKey &key = keyboard[cursorRow][cursorCol];
|
||||||
|
|
||||||
|
// Don't handle press if the key is empty (but allow special keys)
|
||||||
|
if (key.character == 0 && key.type == VK_CHAR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For character keys, insert uppercase/alternate character
|
||||||
|
if (key.type == VK_CHAR) {
|
||||||
|
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key.type) {
|
||||||
|
case VK_BACKSPACE:
|
||||||
|
// One-shot: delete up to 5 characters on long press
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
if (inputText.empty())
|
||||||
|
break;
|
||||||
|
deleteCharacter();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_ENTER:
|
||||||
|
submitText();
|
||||||
|
break;
|
||||||
|
case VK_SPACE:
|
||||||
|
insertCharacter(' ');
|
||||||
|
break;
|
||||||
|
case VK_ESC:
|
||||||
|
if (onTextEntered) {
|
||||||
|
onTextEntered("");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::insertCharacter(char c)
|
||||||
|
{
|
||||||
|
if (inputText.length() < 160) { // Reasonable text length limit
|
||||||
|
inputText += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::deleteCharacter()
|
||||||
|
{
|
||||||
|
if (!inputText.empty()) {
|
||||||
|
inputText.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::submitText()
|
||||||
|
{
|
||||||
|
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
|
||||||
|
|
||||||
|
// Only submit if text is not empty
|
||||||
|
if (!inputText.empty() && onTextEntered) {
|
||||||
|
// Store callback and text to submit before clearing callback
|
||||||
|
std::function<void(const std::string &)> callback = onTextEntered;
|
||||||
|
std::string textToSubmit = inputText;
|
||||||
|
onTextEntered = nullptr;
|
||||||
|
// Don't clear inputText here - let the calling module handle cleanup
|
||||||
|
// inputText = ""; // Removed: keep text visible until module cleans up
|
||||||
|
callback(textToSubmit);
|
||||||
|
} else if (inputText.empty()) {
|
||||||
|
// For empty text, just ignore the submission - don't clear callback
|
||||||
|
// This keeps the virtual keyboard responsive for further input
|
||||||
|
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
|
||||||
|
} else {
|
||||||
|
// No callback available
|
||||||
|
if (screen) {
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::setInputText(const std::string &text)
|
||||||
|
{
|
||||||
|
inputText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string VirtualKeyboard::getInputText() const
|
||||||
|
{
|
||||||
|
return inputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::setHeader(const std::string &header)
|
||||||
|
{
|
||||||
|
headerText = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
|
||||||
|
{
|
||||||
|
onTextEntered = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VirtualKeyboard::resetTimeout()
|
||||||
|
{
|
||||||
|
lastActivityTime = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VirtualKeyboard::isTimedOut() const
|
||||||
|
{
|
||||||
|
return (millis() - lastActivityTime) > TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
80
src/graphics/VirtualKeyboard.h
Normal file
80
src/graphics/VirtualKeyboard.h
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
|
||||||
|
|
||||||
|
struct VirtualKey {
|
||||||
|
char character;
|
||||||
|
VirtualKeyType type;
|
||||||
|
uint8_t x;
|
||||||
|
uint8_t y;
|
||||||
|
uint8_t width;
|
||||||
|
uint8_t height;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VirtualKeyboard
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VirtualKeyboard();
|
||||||
|
~VirtualKeyboard();
|
||||||
|
|
||||||
|
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||||
|
void setInputText(const std::string &text);
|
||||||
|
std::string getInputText() const;
|
||||||
|
void setHeader(const std::string &header);
|
||||||
|
void setCallback(std::function<void(const std::string &)> callback);
|
||||||
|
|
||||||
|
// Navigation methods for encoder input
|
||||||
|
void moveCursorUp();
|
||||||
|
void moveCursorDown();
|
||||||
|
void moveCursorLeft();
|
||||||
|
void moveCursorRight();
|
||||||
|
void handlePress();
|
||||||
|
void handleLongPress();
|
||||||
|
|
||||||
|
// Timeout management
|
||||||
|
void resetTimeout();
|
||||||
|
bool isTimedOut() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const uint8_t KEYBOARD_ROWS = 4;
|
||||||
|
static const uint8_t KEYBOARD_COLS = 11;
|
||||||
|
static const uint8_t KEY_WIDTH = 9;
|
||||||
|
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||||
|
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||||
|
|
||||||
|
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||||
|
|
||||||
|
std::string inputText;
|
||||||
|
std::string headerText;
|
||||||
|
std::function<void(const std::string &)> onTextEntered;
|
||||||
|
|
||||||
|
uint8_t cursorRow;
|
||||||
|
uint8_t cursorCol;
|
||||||
|
|
||||||
|
// Timeout management for auto-exit
|
||||||
|
uint32_t lastActivityTime;
|
||||||
|
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||||
|
|
||||||
|
void initializeKeyboard();
|
||||||
|
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
|
||||||
|
bool isLastCol);
|
||||||
|
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||||
|
|
||||||
|
// Unified cursor movement helper
|
||||||
|
void moveCursorDelta(int dRow, int dCol);
|
||||||
|
|
||||||
|
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||||
|
void insertCharacter(char c);
|
||||||
|
void deleteCharacter();
|
||||||
|
void submitText();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
@@ -146,6 +146,7 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
|||||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||||
{
|
{
|
||||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
@@ -179,21 +180,22 @@ void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool
|
|||||||
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
// Draw a digital clock
|
// Draw a digital clock
|
||||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->clear();
|
display->clear();
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
int line = 1;
|
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
|
||||||
graphics::ClockRenderer::digitalWatchFace, 1);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
@@ -216,7 +218,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
hour %= 12;
|
hour %= 12;
|
||||||
if (hour == 0)
|
if (hour == 0)
|
||||||
hour = 12;
|
hour = 12;
|
||||||
bool isPM = hour >= 12;
|
|
||||||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||||
} else {
|
} else {
|
||||||
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||||
@@ -228,9 +229,11 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
float scale = 1.5;
|
float scale = 1.5;
|
||||||
|
#elif defined(CHATTER_2)
|
||||||
|
float scale = 1.1;
|
||||||
#else
|
#else
|
||||||
float scale = 0.75;
|
float scale = 0.75;
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
scale = 1.5;
|
scale = 1.5;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -276,17 +279,23 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
|
|
||||||
// draw seconds string
|
// draw seconds string
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1;
|
int xOffset = (isHighResolution) ? 0 : -1;
|
||||||
if (hour >= 10) {
|
if (hour >= 10) {
|
||||||
xOffset += (SCREEN_WIDTH > 128) ? 32 : 18;
|
xOffset += (isHighResolution) ? 32 : 18;
|
||||||
}
|
}
|
||||||
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
|
#ifdef SENSECAP_INDICATOR
|
||||||
|
yOffset -= 3;
|
||||||
|
#endif
|
||||||
|
#ifdef T_DECK
|
||||||
|
yOffset -= 5;
|
||||||
|
#endif
|
||||||
if (config.display.use_12h_clock) {
|
if (config.display.use_12h_clock) {
|
||||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||||
isPM ? "pm" : "am");
|
isPM ? "pm" : "am");
|
||||||
}
|
}
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
xOffset = (SCREEN_WIDTH > 128) ? 18 : 10;
|
xOffset = (isHighResolution) ? 18 : 10;
|
||||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||||
secondString);
|
secondString);
|
||||||
#endif
|
#endif
|
||||||
@@ -301,31 +310,30 @@ void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
graphics::UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus);
|
|
||||||
|
|
||||||
if (powerStatus->getHasBattery()) {
|
|
||||||
char batteryPercent[8];
|
|
||||||
snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent());
|
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
|
|
||||||
display->drawString(x + 20, y + 2, batteryPercent);
|
|
||||||
}
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
|
||||||
graphics::ClockRenderer::digitalWatchFace, 1);
|
|
||||||
|
|
||||||
// clock face center coordinates
|
// clock face center coordinates
|
||||||
int16_t centerX = display->getWidth() / 2;
|
int16_t centerX = display->getWidth() / 2;
|
||||||
int16_t centerY = display->getHeight() / 2;
|
int16_t centerY = display->getHeight() / 2;
|
||||||
|
|
||||||
// clock face radius
|
// clock face radius
|
||||||
int16_t radius = (display->getWidth() / 2) * 0.8;
|
int16_t radius = 0;
|
||||||
|
if (display->getHeight() < display->getWidth()) {
|
||||||
|
radius = (display->getHeight() / 2) * 0.9;
|
||||||
|
} else {
|
||||||
|
radius = (display->getWidth() / 2) * 0.9;
|
||||||
|
}
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
radius = (display->getWidth() / 2) * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
// noon (0 deg) coordinates (outermost circle)
|
// noon (0 deg) coordinates (outermost circle)
|
||||||
int16_t noonX = centerX;
|
int16_t noonX = centerX;
|
||||||
@@ -338,10 +346,16 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||||
|
|
||||||
// seconds tick mark inner y coordinate; (second nested circle)
|
// seconds tick mark inner y coordinate; (second nested circle)
|
||||||
double secondsTickMarkInnerNoonY = (double)noonY + 8;
|
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||||
|
if (isHighResolution) {
|
||||||
|
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||||
|
}
|
||||||
|
|
||||||
// hours tick mark inner y coordinate; (third nested circle)
|
// hours tick mark inner y coordinate; (third nested circle)
|
||||||
double hoursTickMarkInnerNoonY = (double)noonY + 16;
|
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||||
|
if (isHighResolution) {
|
||||||
|
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||||
|
}
|
||||||
|
|
||||||
// minute hand y coordinate
|
// minute hand y coordinate
|
||||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||||
@@ -350,7 +364,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||||||
|
|
||||||
// hour hand radius and y coordinate
|
// hour hand radius and y coordinate
|
||||||
int16_t hourHandRadius = radius * 0.55;
|
int16_t hourHandRadius = radius * 0.35;
|
||||||
|
if (isHighResolution) {
|
||||||
|
hourHandRadius = radius * 0.55;
|
||||||
|
}
|
||||||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||||
|
|
||||||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||||||
@@ -366,7 +383,20 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
hour = hour > 12 ? hour - 12 : hour;
|
bool isPM = hour >= 12;
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
isPM = hour >= 12;
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int yOffset = isHighResolution ? 1 : 0;
|
||||||
|
#ifdef USE_EINK
|
||||||
|
yOffset += 3;
|
||||||
|
#endif
|
||||||
|
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
|
||||||
|
isPM ? "pm" : "am");
|
||||||
|
}
|
||||||
|
hour %= 12;
|
||||||
|
if (hour == 0)
|
||||||
|
hour = 12;
|
||||||
|
|
||||||
int16_t degreesPerHour = 30;
|
int16_t degreesPerHour = 30;
|
||||||
int16_t degreesPerMinuteOrSecond = 6;
|
int16_t degreesPerMinuteOrSecond = 6;
|
||||||
@@ -443,16 +473,32 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||||||
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
// draw hour number
|
// draw hour number
|
||||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
#else
|
||||||
|
#ifdef USE_EINK
|
||||||
|
if (isHighResolution) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (angle % degreesPerMinuteOrSecond == 0) {
|
if (angle % degreesPerMinuteOrSecond == 0) {
|
||||||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||||
|
|
||||||
// draw minute tick mark
|
if (isHighResolution) {
|
||||||
display->drawLine(startX, startY, endX, endY);
|
// draw minute tick mark
|
||||||
|
display->drawLine(startX, startY, endX, endY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,9 +507,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
// draw minute hand
|
// draw minute hand
|
||||||
display->drawLine(centerX, centerY, minuteX, minuteY);
|
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||||||
|
#ifndef USE_EINK
|
||||||
// draw second hand
|
// draw second hand
|
||||||
display->drawLine(centerX, centerY, secondX, secondY);
|
display->drawLine(centerX, centerY, secondX, secondY);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ class Screen;
|
|||||||
|
|
||||||
namespace ClockRenderer
|
namespace ClockRenderer
|
||||||
{
|
{
|
||||||
// Whether we are showing the digital watch face or the analog one
|
|
||||||
static bool digitalWatchFace = true;
|
|
||||||
|
|
||||||
// Clock frame functions
|
// Clock frame functions
|
||||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
@@ -25,7 +23,7 @@ void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int he
|
|||||||
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||||
|
|
||||||
// UI elements for clock displays
|
// UI elements for clock displays
|
||||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
||||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
|
|
||||||
} // namespace ClockRenderer
|
} // namespace ClockRenderer
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -45,17 +46,18 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
// This could draw a "N" indicator or north arrow
|
// This could draw a "N" indicator or north arrow
|
||||||
// For now, we'll draw a simple north indicator
|
// For now, we'll draw a simple north indicator
|
||||||
// const float radius = 17.0f;
|
// const float radius = 17.0f;
|
||||||
if (display->width() > 128) {
|
if (isHighResolution) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
Point north(0, -radius);
|
Point north(0, -radius);
|
||||||
north.rotate(-myHeading);
|
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||||
|
north.rotate(-myHeading);
|
||||||
north.translate(compassX, compassY);
|
north.translate(compassX, compassY);
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
if (display->width() > 128) {
|
if (isHighResolution) {
|
||||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
@@ -91,18 +93,22 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
|
|||||||
float radians = bearing * DEG_TO_RAD;
|
float radians = bearing * DEG_TO_RAD;
|
||||||
|
|
||||||
Point tip(0, -size / 2);
|
Point tip(0, -size / 2);
|
||||||
Point left(-size / 4, size / 4);
|
Point left(-size / 6, size / 4);
|
||||||
Point right(size / 4, size / 4);
|
Point right(size / 6, size / 4);
|
||||||
|
Point tail(0, size / 4.5);
|
||||||
|
|
||||||
tip.rotate(radians);
|
tip.rotate(radians);
|
||||||
left.rotate(radians);
|
left.rotate(radians);
|
||||||
right.rotate(radians);
|
right.rotate(radians);
|
||||||
|
tail.rotate(radians);
|
||||||
|
|
||||||
tip.translate(x, y);
|
tip.translate(x, y);
|
||||||
left.translate(x, y);
|
left.translate(x, y);
|
||||||
right.translate(x, y);
|
right.translate(x, y);
|
||||||
|
tail.translate(x, y);
|
||||||
|
|
||||||
display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y);
|
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
||||||
|
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
float estimatedHeading(double lat, double lon)
|
float estimatedHeading(double lat, double lon)
|
||||||
@@ -127,14 +133,5 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
|||||||
return maxDiam;
|
return maxDiam;
|
||||||
}
|
}
|
||||||
|
|
||||||
float calculateBearing(double lat1, double lon1, double lat2, double lon2)
|
|
||||||
{
|
|
||||||
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
|
||||||
double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
|
|
||||||
double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
|
|
||||||
double bearing = atan2(y, x) * RAD_TO_DEG;
|
|
||||||
return fmod(bearing + 360.0, 360.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace CompassRenderer
|
} // namespace CompassRenderer
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
|
|||||||
float estimatedHeading(double lat, double lon);
|
float estimatedHeading(double lat, double lon);
|
||||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||||
|
|
||||||
// Utility functions for bearing calculations
|
|
||||||
float calculateBearing(double lat1, double lon1, double lat2, double lon2);
|
|
||||||
|
|
||||||
} // namespace CompassRenderer
|
} // namespace CompassRenderer
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -67,21 +67,6 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
|||||||
|
|
||||||
char channelStr[20];
|
char channelStr[20];
|
||||||
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
||||||
|
|
||||||
// Display power status
|
|
||||||
if (powerStatus->getHasBattery()) {
|
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
|
||||||
UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus);
|
|
||||||
} else {
|
|
||||||
UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus);
|
|
||||||
}
|
|
||||||
} else if (powerStatus->knowsUSB()) {
|
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
|
||||||
display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
|
||||||
} else {
|
|
||||||
display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Display nodes status
|
// Display nodes status
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||||
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
||||||
@@ -393,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
|
|
||||||
// === Set Title
|
// === Set Title
|
||||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa";
|
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
@@ -427,9 +412,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
float freq = RadioLibInterface::instance->getFreq();
|
float freq = RadioLibInterface::instance->getFreq();
|
||||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||||
if (config.lora.channel_num == 0) {
|
if (config.lora.channel_num == 0) {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
|
||||||
} else {
|
} else {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||||
}
|
}
|
||||||
size_t len = strlen(frequencyslot);
|
size_t len = strlen(frequencyslot);
|
||||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||||
@@ -444,12 +429,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
char chUtilPercentage[10];
|
char chUtilPercentage[10];
|
||||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
|
||||||
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
|
int chutil_bar_width = (isHighResolution) ? 100 : 50;
|
||||||
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
|
int chutil_bar_height = (isHighResolution) ? 12 : 7;
|
||||||
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
|
int extraoffset = (isHighResolution) ? 6 : 3;
|
||||||
int chutil_percent = airTime->channelUtilizationPercent();
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
|
||||||
int centerofscreen = SCREEN_WIDTH / 2;
|
int centerofscreen = SCREEN_WIDTH / 2;
|
||||||
@@ -498,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ****************************
|
// ****************************
|
||||||
// * Memory Screen *
|
// * System Screen *
|
||||||
// ****************************
|
// ****************************
|
||||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
@@ -516,7 +501,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
const int barHeight = 6;
|
const int barHeight = 6;
|
||||||
const int labelX = x;
|
const int labelX = x;
|
||||||
const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0;
|
int barsOffset = (isHighResolution) ? 24 : 0;
|
||||||
|
#ifdef USE_EINK
|
||||||
|
barsOffset -= 12;
|
||||||
|
#endif
|
||||||
const int barX = x + 40 + barsOffset;
|
const int barX = x + 40 + barsOffset;
|
||||||
|
|
||||||
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||||
@@ -526,7 +514,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int percent = (used * 100) / total;
|
int percent = (used * 100) / total;
|
||||||
|
|
||||||
char combinedStr[24];
|
char combinedStr[24];
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
||||||
total / 1024);
|
total / 1024);
|
||||||
} else {
|
} else {
|
||||||
@@ -605,7 +593,19 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
}
|
}
|
||||||
line += 1;
|
line += 1;
|
||||||
char appversionstr[35];
|
char appversionstr[35];
|
||||||
snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION));
|
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
|
||||||
|
char appversionstr_formatted[40];
|
||||||
|
char *lastDot = strrchr(appversionstr, '.');
|
||||||
|
if (lastDot) {
|
||||||
|
size_t prefixLen = lastDot - appversionstr;
|
||||||
|
strncpy(appversionstr_formatted, appversionstr, prefixLen);
|
||||||
|
appversionstr_formatted[prefixLen] = '\0';
|
||||||
|
strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1);
|
||||||
|
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
|
||||||
|
appversionstr[sizeof(appversionstr) - 1] = '\0';
|
||||||
|
}
|
||||||
int textWidth = display->getStringWidth(appversionstr);
|
int textWidth = display->getStringWidth(appversionstr);
|
||||||
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user