mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-15 23:32:34 +00:00
Compare commits
501 Commits
arduino-es
...
agc-reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2d8eac6a | ||
|
|
034d2dd025 | ||
|
|
b8bfed2810 | ||
|
|
c4d7ad2190 | ||
|
|
910fe911f8 | ||
|
|
9ab9650248 | ||
|
|
37a0f774a2 | ||
|
|
a71b47b5bb | ||
|
|
9df5aa8c70 | ||
|
|
130833b5be | ||
|
|
fe6509a0f2 | ||
|
|
fcaa168d2d | ||
|
|
7537d28419 | ||
|
|
26f25069dd | ||
|
|
5eeffdb290 | ||
|
|
5d71776527 | ||
|
|
661e596dbb | ||
|
|
a6732682de | ||
|
|
f0126d44e2 | ||
|
|
fb08e17c39 | ||
|
|
11aff46af1 | ||
|
|
cb11e6b720 | ||
|
|
464663b496 | ||
|
|
981d058e9f | ||
|
|
9056915e7b | ||
|
|
554112ceb5 | ||
|
|
29458cd8c4 | ||
|
|
8bf32dc042 | ||
|
|
73cadce581 | ||
|
|
eee80ce636 | ||
|
|
30d6962e79 | ||
|
|
f99747180e | ||
|
|
91d928d4c5 | ||
|
|
64bfe73c06 | ||
|
|
fca5343460 | ||
|
|
cafb007ec4 | ||
|
|
694b669eb7 | ||
|
|
7899340131 | ||
|
|
0bb1c1fe6f | ||
|
|
05febc25e1 | ||
|
|
e5a2ce54e7 | ||
|
|
45f15b8fe6 | ||
|
|
2a14696525 | ||
|
|
05edcc5d6c | ||
|
|
828e11cc48 | ||
|
|
adae68fbfe | ||
|
|
7822f28152 | ||
|
|
d332dfa19b | ||
|
|
fe2e2753aa | ||
|
|
fcb1d64eb9 | ||
|
|
0c2673ee2f | ||
|
|
9c5513dcfe | ||
|
|
74e6723ad9 | ||
|
|
e8e8ee0993 | ||
|
|
a7f15097da | ||
|
|
e4ec719e6f | ||
|
|
9b7b8ffb21 | ||
|
|
f0e4ea7664 | ||
|
|
468b40e8db | ||
|
|
bd9076b740 | ||
|
|
81a5aeff74 | ||
|
|
f13e7c20ba | ||
|
|
5bcc47dddb | ||
|
|
668cc9fd64 | ||
|
|
1d5b343836 | ||
|
|
8023f475ee | ||
|
|
b696e083f3 | ||
|
|
b214f09ca1 | ||
|
|
68a2c4adda | ||
|
|
518680514f | ||
|
|
fc1737c949 | ||
|
|
735784e6e4 | ||
|
|
87e3540f48 | ||
|
|
329a494ce2 | ||
|
|
627c0145e7 | ||
|
|
29f4d99bf6 | ||
|
|
036a58735e | ||
|
|
18ca9e80d5 | ||
|
|
5c2997ef53 | ||
|
|
c147ce9a85 | ||
|
|
27f316b931 | ||
|
|
f7cf5e6b0a | ||
|
|
7c373b76c4 | ||
|
|
de6a02756d | ||
|
|
d708ed5908 | ||
|
|
7c4367cddc | ||
|
|
6022b749ba | ||
|
|
cbd30f95f3 | ||
|
|
9ded6a5215 | ||
|
|
1e4bcb04d5 | ||
|
|
c4dff21e5b | ||
|
|
888692a373 | ||
|
|
fe4fb085e6 | ||
|
|
7c5e2bc95a | ||
|
|
ed32650b9b | ||
|
|
1b97cf57ad | ||
|
|
e8296914a5 | ||
|
|
0e38fef5bf | ||
|
|
b7f6a2acb6 | ||
|
|
0c2283e19e | ||
|
|
78d010fd29 | ||
|
|
037e56b1fd | ||
|
|
c7208ca05b | ||
|
|
f72a4c50bd | ||
|
|
775595cb37 | ||
|
|
047600d088 | ||
|
|
560eb2c455 | ||
|
|
1be3820152 | ||
|
|
da98622f59 | ||
|
|
03baad2c11 | ||
|
|
0ddaf710e4 | ||
|
|
50cfe7c705 | ||
|
|
76c1d69560 | ||
|
|
17863e96e2 | ||
|
|
c48a64e183 | ||
|
|
e954591ca5 | ||
|
|
305f513834 | ||
|
|
0860fee209 | ||
|
|
878ac3ec84 | ||
|
|
a62e1cfa3c | ||
|
|
ca02808c5d | ||
|
|
b978c6c86c | ||
|
|
51ad9d0244 | ||
|
|
76d4807130 | ||
|
|
5ec09783c5 | ||
|
|
2eb0fcbcaf | ||
|
|
9bb7bb467b | ||
|
|
de67714248 | ||
|
|
e0cf9130be | ||
|
|
f82667d71e | ||
|
|
1d283523f2 | ||
|
|
ec28c383af | ||
|
|
641a2fc63d | ||
|
|
f7469159cf | ||
|
|
af83670376 | ||
|
|
849bbad279 | ||
|
|
b28d095096 | ||
|
|
17afdb9ccf | ||
|
|
d5164b4fbf | ||
|
|
ad44940732 | ||
|
|
8b466b1db3 | ||
|
|
f9937967fa | ||
|
|
4fd568f384 | ||
|
|
dae9b1c024 | ||
|
|
a8a6644192 | ||
|
|
34a595b88e | ||
|
|
e32ce3fafe | ||
|
|
69c61f8247 | ||
|
|
b08e4efb78 | ||
|
|
ee6857511a | ||
|
|
9df4d57168 | ||
|
|
500e7920ae | ||
|
|
ee8fa9f328 | ||
|
|
02efef3aaf | ||
|
|
0f6131d2c8 | ||
|
|
8d323a1cf1 | ||
|
|
a3e6f16378 | ||
|
|
971543fab3 | ||
|
|
85fe7d26ed | ||
|
|
a1c658a467 | ||
|
|
777e11bad9 | ||
|
|
a15d654767 | ||
|
|
033fc0c8f3 | ||
|
|
7633ddcfd1 | ||
|
|
8717c60f13 | ||
|
|
067939ca24 | ||
|
|
abc011aeb9 | ||
|
|
a4a6ee1df4 | ||
|
|
6448f069f8 | ||
|
|
f6a28e15d2 | ||
|
|
2b60bae61c | ||
|
|
bc516ebbac | ||
|
|
045176789e | ||
|
|
667b7c50e2 | ||
|
|
9b1a118103 | ||
|
|
64c268f055 | ||
|
|
806bf6ce2c | ||
|
|
7eb0109e33 | ||
|
|
4dec912a39 | ||
|
|
73147c4028 | ||
|
|
e8627b2d01 | ||
|
|
ab00e991f6 | ||
|
|
a2d86454d3 | ||
|
|
bc3db1b5c1 | ||
|
|
2f1198ddf3 | ||
|
|
0624059683 | ||
|
|
52ee655fd2 | ||
|
|
9980c56d81 | ||
|
|
fc9f1ac056 | ||
|
|
c65dbe490e | ||
|
|
44636cc9f5 | ||
|
|
aa876ba42f | ||
|
|
12c3ddf457 | ||
|
|
d9f0590f8e | ||
|
|
191d20ed04 | ||
|
|
79bc286b35 | ||
|
|
8d9fda38d6 | ||
|
|
3c25652cdf | ||
|
|
9b3d76967b | ||
|
|
fd5ca8b73c | ||
|
|
18058ef507 | ||
|
|
d9ba0633f6 | ||
|
|
68fc931518 | ||
|
|
0ad6b813fc | ||
|
|
d41fb7bcb5 | ||
|
|
fef4a2987d | ||
|
|
1fc8d54d4c | ||
|
|
47a82bdb98 | ||
|
|
8ed6514771 | ||
|
|
17ecd69416 | ||
|
|
bb6f19dddf | ||
|
|
2fdc0d0928 | ||
|
|
85cdcad194 | ||
|
|
0b4a28866b | ||
|
|
91e2e3f0e8 | ||
|
|
14e64d6b9e | ||
|
|
58602d59bd | ||
|
|
d461eb35fc | ||
|
|
371313080b | ||
|
|
db55d8a59d | ||
|
|
949f881ae8 | ||
|
|
ca3c45a2f3 | ||
|
|
c33c368315 | ||
|
|
1835ff2d78 | ||
|
|
8e04f9f631 | ||
|
|
83be632a1a | ||
|
|
1ed7aad976 | ||
|
|
edb5c0f88e | ||
|
|
060a129995 | ||
|
|
a1ca553bc0 | ||
|
|
189aec9fe3 | ||
|
|
94d4bdf05c | ||
|
|
1968a009dd | ||
|
|
8e608e8186 | ||
|
|
d998f70b56 | ||
|
|
f55db903b2 | ||
|
|
91efaba389 | ||
|
|
a8c66547cc | ||
|
|
f77ca2533b | ||
|
|
07b58a82d5 | ||
|
|
e1485b530f | ||
|
|
2fbfb19304 | ||
|
|
e7840122e8 | ||
|
|
a4e09aa9da | ||
|
|
db941bff3b | ||
|
|
13e1f99c7e | ||
|
|
319cd6fa7b | ||
|
|
0db2e40ee3 | ||
|
|
59f9e2a00b | ||
|
|
97d0f3286e | ||
|
|
388c821028 | ||
|
|
3d51287ba7 | ||
|
|
1e1f2a69b7 | ||
|
|
da4bc0f97c | ||
|
|
b3df32c6c5 | ||
|
|
cea9e1238b | ||
|
|
11eb4a5b90 | ||
|
|
d1fd102952 | ||
|
|
27b07cd1c5 | ||
|
|
5701755608 | ||
|
|
d09baddce5 | ||
|
|
c42513d7c8 | ||
|
|
2010871e4b | ||
|
|
c811e4c573 | ||
|
|
040b3b8c7f | ||
|
|
d558df8a3a | ||
|
|
4100ba83a3 | ||
|
|
b49496d99d | ||
|
|
34c2191f63 | ||
|
|
52527e281d | ||
|
|
9b6cf53730 | ||
|
|
6a3b2ceafe | ||
|
|
a76cc88dc2 | ||
|
|
3463006f73 | ||
|
|
db2f79b6c4 | ||
|
|
1d3c47c5fa | ||
|
|
44968415a5 | ||
|
|
8db9b24934 | ||
|
|
1fc07607cb | ||
|
|
22b71a1e95 | ||
|
|
2ccf91f443 | ||
|
|
58e4dcea61 | ||
|
|
6f56ccd283 | ||
|
|
bfb03b422a | ||
|
|
cc3ff1504a | ||
|
|
9b6a7ed3bb | ||
|
|
fdc8796052 | ||
|
|
787642ad4c | ||
|
|
54f9f7a591 | ||
|
|
0e26702c46 | ||
|
|
6677255f6c | ||
|
|
3a63a56cff | ||
|
|
edb250e782 | ||
|
|
f32e06a321 | ||
|
|
8095261dfd | ||
|
|
72b9a02f3e | ||
|
|
af26408d73 | ||
|
|
c8f69913d6 | ||
|
|
42fbb62f18 | ||
|
|
e6adb197e4 | ||
|
|
cfb34a8816 | ||
|
|
8264d4d65e | ||
|
|
c11680fcc0 | ||
|
|
479c1f5346 | ||
|
|
a1cf305336 | ||
|
|
7b2ff7e196 | ||
|
|
b1d314db1e | ||
|
|
cc579dd0bd | ||
|
|
1ac2382d7c | ||
|
|
2ef5b968f9 | ||
|
|
6a92358b68 | ||
|
|
e20a91b945 | ||
|
|
3fbe7fd8b2 | ||
|
|
8841c1540d | ||
|
|
e2ce369782 | ||
|
|
901bcc24ee | ||
|
|
b14e5770d5 | ||
|
|
68ba3b315c | ||
|
|
2bafac242e | ||
|
|
39648e609a | ||
|
|
dcd53eb7cb | ||
|
|
7821919fae | ||
|
|
f083864f1f | ||
|
|
8e1da8561e | ||
|
|
953fcca304 | ||
|
|
20bd237ff6 | ||
|
|
c73fe85ec8 | ||
|
|
b13d023d58 | ||
|
|
9345bdcb22 | ||
|
|
902405a985 | ||
|
|
ec29100a88 | ||
|
|
017d07e108 | ||
|
|
89cccdbbe2 | ||
|
|
8f0e17a653 | ||
|
|
e3772858b3 | ||
|
|
c71c1f2d15 | ||
|
|
2567c03a3f | ||
|
|
d8381aa905 | ||
|
|
188283b382 | ||
|
|
953fdc9eed | ||
|
|
ec7415b3fd | ||
|
|
a70ffae82c | ||
|
|
6a8732bbaa | ||
|
|
ba18467bd1 | ||
|
|
d427b477e3 | ||
|
|
46317f413a | ||
|
|
f16aa730d3 | ||
|
|
22fcd102a0 | ||
|
|
c9cb2cfd94 | ||
|
|
d31e3839fb | ||
|
|
43078a40eb | ||
|
|
4ac99c5df1 | ||
|
|
c9702fe4d0 | ||
|
|
e0f88be2d7 | ||
|
|
1c256ccfd7 | ||
|
|
b9d53d667e | ||
|
|
5d3c92f1a2 | ||
|
|
6c932d51ec | ||
|
|
f0b7aab030 | ||
|
|
5fca3a30ec | ||
|
|
a76f591231 | ||
|
|
20f68929c8 | ||
|
|
42e4759634 | ||
|
|
3d86c99c25 | ||
|
|
09de0e3edb | ||
|
|
00772996b6 | ||
|
|
de3a65579d | ||
|
|
90ddbf6f2c | ||
|
|
566c2c3fdf | ||
|
|
bfadd9c866 | ||
|
|
f8d44f8f6c | ||
|
|
ccff2769fe | ||
|
|
e6bfc4a97a | ||
|
|
a297d21707 | ||
|
|
a8cf4dfe2d | ||
|
|
ead67446a3 | ||
|
|
43cf12edfb | ||
|
|
106a052950 | ||
|
|
0fc33c352a | ||
|
|
e0890b2a13 | ||
|
|
5579d87845 | ||
|
|
35340fc6e2 | ||
|
|
4ab125bbf7 | ||
|
|
87eff2c4a9 | ||
|
|
527e88ca46 | ||
|
|
4140ecfb49 | ||
|
|
27cdd464d1 | ||
|
|
5a463373f2 | ||
|
|
b768860866 | ||
|
|
c63102a312 | ||
|
|
b1f55ef6e8 | ||
|
|
b305acf7e5 | ||
|
|
ab5332950c | ||
|
|
484b4cd848 | ||
|
|
3cc2b70e4f | ||
|
|
7e00054fd7 | ||
|
|
693181b2be | ||
|
|
39c663f203 | ||
|
|
71f659cba6 | ||
|
|
4e879a7b26 | ||
|
|
8c9c00172c | ||
|
|
d5300a1141 | ||
|
|
83ae72cbb2 | ||
|
|
a31fdf01ce | ||
|
|
1594421214 | ||
|
|
02cb306bb1 | ||
|
|
6b7ad9c4e1 | ||
|
|
fa1ccf4779 | ||
|
|
67ecb60bcd | ||
|
|
9da92626e5 | ||
|
|
6f7149e9a2 | ||
|
|
95dc61f57b | ||
|
|
0aa48c9c22 | ||
|
|
088318512a | ||
|
|
ca4b98f2b1 | ||
|
|
1643249db7 | ||
|
|
2191fe465c | ||
|
|
b75e8913e0 | ||
|
|
87a1449f76 | ||
|
|
569a911455 | ||
|
|
c5b95f5a4b | ||
|
|
15f4aebcd5 | ||
|
|
2354c52b16 | ||
|
|
227d0fa7dc | ||
|
|
7c1eff54fb | ||
|
|
c92fa6aa8a | ||
|
|
77acbc6814 | ||
|
|
81cb1e427f | ||
|
|
f6ba9604a7 | ||
|
|
9c6544ebfa | ||
|
|
b6eeccadeb | ||
|
|
e7b7479589 | ||
|
|
e1634076f2 | ||
|
|
d6df664102 | ||
|
|
50a5b36498 | ||
|
|
4d6fe936ae | ||
|
|
f825e61b89 | ||
|
|
64cd62d6af | ||
|
|
f31fd34ce0 | ||
|
|
26bcc9627d | ||
|
|
cc37535b2d | ||
|
|
ced334d13b | ||
|
|
521fbc44b4 | ||
|
|
361771c9bb | ||
|
|
fa45660b7d | ||
|
|
2e8f4ad6af | ||
|
|
18550ea80c | ||
|
|
1c1c0cc791 | ||
|
|
789c1ab59d | ||
|
|
5850a7cd6b | ||
|
|
6c89ea7cee | ||
|
|
c62f262f63 | ||
|
|
798040b5b8 | ||
|
|
ba582d6ef4 | ||
|
|
bbf6f01d42 | ||
|
|
142abb2a4e | ||
|
|
0952007805 | ||
|
|
7612799ef6 | ||
|
|
c5fad6cca1 | ||
|
|
b8d7222423 | ||
|
|
16d7de5989 | ||
|
|
102c447fe3 | ||
|
|
d66665b96e | ||
|
|
088be6bf6a | ||
|
|
ca79760372 | ||
|
|
4a669032dc | ||
|
|
b53dd2ec90 | ||
|
|
a0e14439cb | ||
|
|
10c6836263 | ||
|
|
9b41131af8 | ||
|
|
fb34dac08d | ||
|
|
5f8503c62d | ||
|
|
dd2f77ea0c | ||
|
|
46f797c40d | ||
|
|
75b01e17bc | ||
|
|
8685436cbb | ||
|
|
67e3a17b28 | ||
|
|
24204feb71 | ||
|
|
4ace2638e1 | ||
|
|
5aa486d6c2 | ||
|
|
ba26d03b1b | ||
|
|
9a1c2c9b61 | ||
|
|
5b9db81819 | ||
|
|
f2ba7d7851 | ||
|
|
1eafdfcbc8 | ||
|
|
103ea2f168 | ||
|
|
4fef890466 | ||
|
|
35f5b7ec03 | ||
|
|
1037fa5622 | ||
|
|
1c329d9ffa | ||
|
|
093a37a2b0 | ||
|
|
4dfcd61d46 | ||
|
|
9d560fe9e1 | ||
|
|
8e32d58077 | ||
|
|
7b24d31636 | ||
|
|
35d9e68053 | ||
|
|
caf2180075 | ||
|
|
236d2b92dc | ||
|
|
e6a2df5b6d | ||
|
|
f6bb1977bc | ||
|
|
9b0fbcf1d9 |
@@ -76,7 +76,7 @@ bool loopCanSleep()
|
|||||||
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
|
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
|
||||||
void lateInitVariant()
|
void lateInitVariant()
|
||||||
{
|
{
|
||||||
settingsMap[logoutputlevel] = level_error;
|
portduino_config.logoutputlevel = level_error;
|
||||||
channelFile.channels[0] = meshtastic_Channel{
|
channelFile.channels[0] = meshtastic_Channel{
|
||||||
.has_settings = true,
|
.has_settings = true,
|
||||||
.settings =
|
.settings =
|
||||||
@@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht
|
|||||||
// Start Meshtastic in a thread and wait till it has reached the ON state.
|
// Start Meshtastic in a thread and wait till it has reached the ON state.
|
||||||
int LLVMFuzzerInitialize(int *argc, char ***argv)
|
int LLVMFuzzerInitialize(int *argc, char ***argv)
|
||||||
{
|
{
|
||||||
settingsMap[maxtophone] = 5;
|
portduino_config.maxtophone = 5;
|
||||||
|
|
||||||
meshtasticThread = std::thread([program = *argv[0]]() {
|
meshtasticThread = std::thread([program = *argv[0]]() {
|
||||||
char nodeIdStr[12];
|
char nodeIdStr[12];
|
||||||
|
|||||||
3
.github/workflows/build_firmware.yml
vendored
3
.github/workflows/build_firmware.yml
vendored
@@ -19,6 +19,8 @@ jobs:
|
|||||||
pio-build:
|
pio-build:
|
||||||
name: build-${{ inputs.platform }}
|
name: build-${{ inputs.platform }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
artifact-id: ${{ steps.upload.outputs.artifact-id }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@@ -55,6 +57,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Store binaries as an artifact
|
- name: Store binaries as an artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
id: upload
|
||||||
with:
|
with:
|
||||||
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
|
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
176
.github/workflows/build_one_arch.yml
vendored
Normal file
176
.github/workflows/build_one_arch.yml
vendored
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
name: Build One Arch
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
# trunk-ignore(checkov/CKV_GHA_7)
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
|
- native
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
env:
|
||||||
|
INPUT_ARCH: ${{ github.event.inputs.arch }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
cache: pip
|
||||||
|
- run: pip install -U platformio
|
||||||
|
- name: Generate matrix
|
||||||
|
id: jsonStep
|
||||||
|
run: |
|
||||||
|
TARGETS=$(./bin/generate_ci_matrix.py $INPUT_ARCH --level extra)
|
||||||
|
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
|
||||||
|
echo "selected_arch=$TARGETS" >> $GITHUB_OUTPUT
|
||||||
|
outputs:
|
||||||
|
selected_arch: ${{ steps.jsonStep.outputs.selected_arch }}
|
||||||
|
|
||||||
|
version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@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 }}
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||||
|
needs: [setup, version]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
build: ${{ fromJson(needs.setup.outputs.selected_arch) }}
|
||||||
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ matrix.build.board }}
|
||||||
|
platform: ${{ matrix.build.arch }}
|
||||||
|
|
||||||
|
build-debian-src:
|
||||||
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
|
||||||
|
uses: ./.github/workflows/build_debian_src.yml
|
||||||
|
with:
|
||||||
|
series: UNRELEASED
|
||||||
|
build_location: local
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
package-pio-deps-native-tft:
|
||||||
|
if: ${{ inputs.arch == 'native' }}
|
||||||
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
|
with:
|
||||||
|
pio_env: native-tft
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test-native:
|
||||||
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }}
|
||||||
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
|
gather-artifacts:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [version, build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
path: ./
|
||||||
|
pattern: firmware-${{inputs.arch}}-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Display structure of downloaded files
|
||||||
|
run: ls -R
|
||||||
|
|
||||||
|
- name: Move files up
|
||||||
|
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||||
|
|
||||||
|
- name: Repackage in single firmware zip
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
|
||||||
|
overwrite: true
|
||||||
|
path: |
|
||||||
|
./firmware-*.bin
|
||||||
|
./firmware-*.uf2
|
||||||
|
./firmware-*.hex
|
||||||
|
./firmware-*-ota.zip
|
||||||
|
./device-*.sh
|
||||||
|
./device-*.bat
|
||||||
|
./littlefs-*.bin
|
||||||
|
./bleota*bin
|
||||||
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
|
||||||
|
merge-multiple: true
|
||||||
|
path: ./output
|
||||||
|
|
||||||
|
# For diagnostics
|
||||||
|
- name: Show artifacts
|
||||||
|
run: ls -lR
|
||||||
|
|
||||||
|
- name: Device scripts permissions
|
||||||
|
run: |
|
||||||
|
chmod +x ./output/device-install.sh
|
||||||
|
chmod +x ./output/device-update.sh
|
||||||
|
|
||||||
|
- name: Zip firmware
|
||||||
|
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
|
- name: Repackage in single elfs zip
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
|
overwrite: true
|
||||||
|
path: ./*.elf
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: scruplelesswizard/comment-artifact@main
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
with:
|
||||||
|
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
|
||||||
|
description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
182
.github/workflows/build_one_target.yml
vendored
Normal file
182
.github/workflows/build_one_target.yml
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
name: Build One Target
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
# trunk-ignore(checkov/CKV_GHA_7)
|
||||||
|
arch:
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
|
- native
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
|
||||||
|
# find-target:
|
||||||
|
# type: boolean
|
||||||
|
# default: true
|
||||||
|
# description: 'Find the available targets'
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
find-targets:
|
||||||
|
if: ${{ inputs.target == '' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- esp32
|
||||||
|
- esp32s3
|
||||||
|
- esp32c3
|
||||||
|
- esp32c6
|
||||||
|
- nrf52840
|
||||||
|
- rp2040
|
||||||
|
- rp2350
|
||||||
|
- stm32
|
||||||
|
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
cache: pip
|
||||||
|
- run: pip install -U platformio
|
||||||
|
- name: Generate matrix
|
||||||
|
id: jsonStep
|
||||||
|
run: |
|
||||||
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
|
||||||
|
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Targets:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo $TARGETS >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
version:
|
||||||
|
if: ${{ inputs.target != '' }}
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
build:
|
||||||
|
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
|
||||||
|
needs: [version]
|
||||||
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.version.outputs.long }}
|
||||||
|
pio_env: ${{ inputs.target }}
|
||||||
|
platform: ${{ inputs.arch }}
|
||||||
|
|
||||||
|
build-debian-src:
|
||||||
|
if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }}
|
||||||
|
uses: ./.github/workflows/build_debian_src.yml
|
||||||
|
with:
|
||||||
|
series: UNRELEASED
|
||||||
|
build_location: local
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
package-pio-deps-native-tft:
|
||||||
|
if: ${{ inputs.arch == 'native' }}
|
||||||
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
|
with:
|
||||||
|
pio_env: native-tft
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test-native:
|
||||||
|
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }}
|
||||||
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
|
gather-artifacts:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [version, build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
path: ./
|
||||||
|
pattern: firmware-*-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Display structure of downloaded files
|
||||||
|
run: ls -R
|
||||||
|
|
||||||
|
- name: Move files up
|
||||||
|
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||||
|
|
||||||
|
- name: Repackage in single firmware zip
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
||||||
|
overwrite: true
|
||||||
|
path: |
|
||||||
|
./firmware-*.bin
|
||||||
|
./firmware-*.uf2
|
||||||
|
./firmware-*.hex
|
||||||
|
./firmware-*-ota.zip
|
||||||
|
./device-*.sh
|
||||||
|
./device-*.bat
|
||||||
|
./littlefs-*.bin
|
||||||
|
./bleota*bin
|
||||||
|
./Meshtastic_nRF52_factory_erase*.uf2
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v5
|
||||||
|
with:
|
||||||
|
pattern: firmware-*-${{ needs.version.outputs.long }}
|
||||||
|
merge-multiple: true
|
||||||
|
path: ./output
|
||||||
|
|
||||||
|
# For diagnostics
|
||||||
|
- name: Show artifacts
|
||||||
|
run: ls -lR
|
||||||
|
|
||||||
|
- name: Device scripts permissions
|
||||||
|
run: |
|
||||||
|
chmod +x ./output/device-install.sh
|
||||||
|
chmod +x ./output/device-update.sh
|
||||||
|
|
||||||
|
- name: Zip firmware
|
||||||
|
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
|
||||||
|
|
||||||
|
- name: Repackage in single elfs zip
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
|
||||||
|
overwrite: true
|
||||||
|
path: ./*.elf
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: scruplelesswizard/comment-artifact@main
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
with:
|
||||||
|
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
|
||||||
|
description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
8
.github/workflows/daily_packaging.yml
vendored
8
.github/workflows/daily_packaging.yml
vendored
@@ -21,18 +21,20 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-multiarch:
|
docker-multiarch:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
uses: ./.github/workflows/docker_manifest.yml
|
uses: ./.github/workflows/docker_manifest.yml
|
||||||
with:
|
with:
|
||||||
release_channel: daily
|
release_channel: daily
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
package-ppa:
|
package-ppa:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
series:
|
series:
|
||||||
- jammy # 22.04
|
- jammy # 22.04 LTS
|
||||||
- noble # 24.04
|
- noble # 24.04 LTS
|
||||||
- plucky # 25.04
|
- plucky # 25.04
|
||||||
- questing # 25.10
|
- questing # 25.10
|
||||||
uses: ./.github/workflows/package_ppa.yml
|
uses: ./.github/workflows/package_ppa.yml
|
||||||
@@ -42,6 +44,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
package-obs:
|
package-obs:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
uses: ./.github/workflows/package_obs.yml
|
uses: ./.github/workflows/package_obs.yml
|
||||||
with:
|
with:
|
||||||
obs_project: network:Meshtastic:daily
|
obs_project: network:Meshtastic:daily
|
||||||
@@ -49,6 +52,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
hook-copr:
|
hook-copr:
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
uses: ./.github/workflows/hook_copr.yml
|
uses: ./.github/workflows/hook_copr.yml
|
||||||
with:
|
with:
|
||||||
copr_project: daily
|
copr_project: daily
|
||||||
|
|||||||
210
.github/workflows/main_matrix.yml
vendored
210
.github/workflows/main_matrix.yml
vendored
@@ -28,17 +28,10 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
arch:
|
arch:
|
||||||
- esp32
|
- all
|
||||||
- esp32s3
|
|
||||||
- esp32c3
|
|
||||||
- esp32c6
|
|
||||||
- nrf52840
|
|
||||||
- rp2040
|
|
||||||
- rp2350
|
|
||||||
- stm32
|
|
||||||
- check
|
- check
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
@@ -54,19 +47,13 @@ jobs:
|
|||||||
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}} pr)
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level 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"
|
||||||
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
|
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
|
||||||
|
echo "$TARGETS" >> $GITHUB_STEP_SUMMARY
|
||||||
outputs:
|
outputs:
|
||||||
esp32: ${{ steps.jsonStep.outputs.esp32 }}
|
all: ${{ steps.jsonStep.outputs.all }}
|
||||||
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
|
|
||||||
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
|
|
||||||
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
|
|
||||||
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
|
|
||||||
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
|
|
||||||
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
|
|
||||||
stm32: ${{ steps.jsonStep.outputs.stm32 }}
|
|
||||||
check: ${{ steps.jsonStep.outputs.check }}
|
check: ${{ steps.jsonStep.outputs.check }}
|
||||||
|
|
||||||
version:
|
version:
|
||||||
@@ -88,105 +75,30 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.check) }}
|
matrix:
|
||||||
|
check: ${{ fromJson(needs.setup.outputs.check) }}
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Build base
|
- name: Build base
|
||||||
id: base
|
id: base
|
||||||
uses: ./.github/actions/setup-base
|
uses: ./.github/actions/setup-base
|
||||||
- name: Check ${{ matrix.board }}
|
- name: Check ${{ matrix.check.board }}
|
||||||
run: bin/check-all.sh ${{ matrix.board }}
|
run: bin/check-all.sh ${{ matrix.check.board }}
|
||||||
|
|
||||||
build-esp32:
|
build:
|
||||||
needs: [setup, version]
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
|
matrix:
|
||||||
|
build: ${{ fromJson(needs.setup.outputs.all) }}
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.version.outputs.long }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
pio_env: ${{ matrix.board }}
|
pio_env: ${{ matrix.build.board }}
|
||||||
platform: esp32
|
platform: ${{ matrix.build.platform }}
|
||||||
|
|
||||||
build-esp32s3:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32s3
|
|
||||||
|
|
||||||
build-esp32c3:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32c3
|
|
||||||
|
|
||||||
build-esp32c6:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32c6
|
|
||||||
|
|
||||||
build-nrf52840:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: nrf52840
|
|
||||||
|
|
||||||
build-rp2040:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
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:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: stm32
|
|
||||||
|
|
||||||
build-debian-src:
|
build-debian-src:
|
||||||
if: github.repository == 'meshtastic/firmware'
|
if: github.repository == 'meshtastic/firmware'
|
||||||
@@ -197,68 +109,41 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
package-pio-deps-native-tft:
|
package-pio-deps-native-tft:
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||||
uses: ./.github/workflows/package_pio_deps.yml
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
with:
|
with:
|
||||||
pio_env: native-tft
|
pio_env: native-tft
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
test-native:
|
test-native:
|
||||||
if: ${{ !contains(github.ref_name, 'event/') }}
|
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
|
||||||
uses: ./.github/workflows/test_native.yml
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
docker-deb-amd64:
|
docker:
|
||||||
uses: ./.github/workflows/docker_build.yml
|
strategy:
|
||||||
with:
|
fail-fast: false
|
||||||
distro: debian
|
matrix:
|
||||||
platform: linux/amd64
|
distro: [debian, alpine]
|
||||||
runs-on: ubuntu-24.04
|
platform: [linux/amd64, linux/arm64, linux/arm/v7]
|
||||||
push: false
|
pio_env: [native, native-tft]
|
||||||
|
exclude:
|
||||||
docker-deb-amd64-tft:
|
- distro: alpine
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
pio_env: native-tft
|
|
||||||
|
|
||||||
docker-alp-amd64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: alpine
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
|
|
||||||
docker-alp-amd64-tft:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: alpine
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
pio_env: native-tft
|
|
||||||
|
|
||||||
docker-deb-arm64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/arm64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
push: false
|
|
||||||
|
|
||||||
docker-deb-armv7:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/arm/v7
|
platform: linux/arm/v7
|
||||||
runs-on: ubuntu-24.04-arm
|
- pio_env: native-tft
|
||||||
|
platform: linux/arm64
|
||||||
|
- pio_env: native-tft
|
||||||
|
platform: linux/arm/v7
|
||||||
|
uses: ./.github/workflows/docker_build.yml
|
||||||
|
with:
|
||||||
|
distro: ${{ matrix.distro }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||||
|
pio_env: ${{ matrix.pio_env }}
|
||||||
push: false
|
push: false
|
||||||
|
|
||||||
gather-artifacts:
|
gather-artifacts:
|
||||||
# trunk-ignore(checkov/CKV2_GHA_1)
|
# trunk-ignore(checkov/CKV2_GHA_1)
|
||||||
|
if: github.repository == 'meshtastic/firmware'
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -275,18 +160,7 @@ jobs:
|
|||||||
- rp2350
|
- rp2350
|
||||||
- stm32
|
- stm32
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs: [version, build]
|
||||||
[
|
|
||||||
version,
|
|
||||||
build-esp32,
|
|
||||||
build-esp32s3,
|
|
||||||
build-esp32c3,
|
|
||||||
build-esp32c6,
|
|
||||||
build-nrf52840,
|
|
||||||
build-rp2040,
|
|
||||||
build-rp2350,
|
|
||||||
build-stm32,
|
|
||||||
]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
@@ -358,7 +232,7 @@ jobs:
|
|||||||
|
|
||||||
release-artifacts:
|
release-artifacts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
needs:
|
needs:
|
||||||
@@ -433,7 +307,7 @@ jobs:
|
|||||||
- rp2350
|
- rp2350
|
||||||
- stm32
|
- stm32
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}}
|
||||||
needs: [release-artifacts, version]
|
needs: [release-artifacts, version]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
236
.github/workflows/merge_queue.yml
vendored
236
.github/workflows/merge_queue.yml
vendored
@@ -7,28 +7,18 @@ on:
|
|||||||
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
|
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
|
||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
env:
|
|
||||||
FAIL_FAST_PER_ARCH: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
arch:
|
arch:
|
||||||
- esp32
|
- all
|
||||||
- esp32s3
|
|
||||||
- esp32c3
|
|
||||||
- esp32c6
|
|
||||||
- nrf52840
|
|
||||||
- rp2040
|
|
||||||
- rp2350
|
|
||||||
- stm32
|
|
||||||
- check
|
- check
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
cache: pip
|
cache: pip
|
||||||
@@ -39,25 +29,18 @@ jobs:
|
|||||||
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}} pr)
|
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level 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"
|
||||||
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
|
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
|
||||||
outputs:
|
outputs:
|
||||||
esp32: ${{ steps.jsonStep.outputs.esp32 }}
|
all: ${{ steps.jsonStep.outputs.all }}
|
||||||
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
|
|
||||||
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
|
|
||||||
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
|
|
||||||
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
|
|
||||||
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
|
|
||||||
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
|
|
||||||
stm32: ${{ steps.jsonStep.outputs.stm32 }}
|
|
||||||
check: ${{ steps.jsonStep.outputs.check }}
|
check: ${{ steps.jsonStep.outputs.check }}
|
||||||
|
|
||||||
version:
|
version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Get release version string
|
- name: Get release version string
|
||||||
run: |
|
run: |
|
||||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||||
@@ -73,105 +56,29 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.check) }}
|
matrix:
|
||||||
|
check: ${{ fromJson(needs.setup.outputs.check) }}
|
||||||
|
|
||||||
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
|
||||||
- name: Check ${{ matrix.board }}
|
- name: Check ${{ matrix.check.board }}
|
||||||
run: bin/check-all.sh ${{ matrix.board }}
|
run: bin/check-all.sh ${{ matrix.check.board }}
|
||||||
|
|
||||||
build-esp32:
|
build:
|
||||||
needs: [setup, version]
|
needs: [setup, version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
matrix:
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
|
build: ${{ fromJson(needs.setup.outputs.all) }}
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
uses: ./.github/workflows/build_firmware.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.version.outputs.long }}
|
version: ${{ needs.version.outputs.long }}
|
||||||
pio_env: ${{ matrix.board }}
|
pio_env: ${{ matrix.build.board }}
|
||||||
platform: esp32
|
platform: ${{ matrix.build.platform }}
|
||||||
|
|
||||||
build-esp32s3:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32s3
|
|
||||||
|
|
||||||
build-esp32c3:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32c3
|
|
||||||
|
|
||||||
build-esp32c6:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: esp32c6
|
|
||||||
|
|
||||||
build-nrf52840:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: nrf52840
|
|
||||||
|
|
||||||
build-rp2040:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: rp2040
|
|
||||||
|
|
||||||
build-rp2350:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
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:
|
|
||||||
needs: [setup, version]
|
|
||||||
strategy:
|
|
||||||
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
|
|
||||||
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
|
|
||||||
uses: ./.github/workflows/build_firmware.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.version.outputs.long }}
|
|
||||||
pio_env: ${{ matrix.board }}
|
|
||||||
platform: stm32
|
|
||||||
|
|
||||||
build-debian-src:
|
build-debian-src:
|
||||||
if: github.repository == 'meshtastic/firmware'
|
if: github.repository == 'meshtastic/firmware'
|
||||||
@@ -192,54 +99,26 @@ jobs:
|
|||||||
if: ${{ !contains(github.ref_name, 'event/') }}
|
if: ${{ !contains(github.ref_name, 'event/') }}
|
||||||
uses: ./.github/workflows/test_native.yml
|
uses: ./.github/workflows/test_native.yml
|
||||||
|
|
||||||
docker-deb-amd64:
|
docker:
|
||||||
uses: ./.github/workflows/docker_build.yml
|
strategy:
|
||||||
with:
|
fail-fast: false
|
||||||
distro: debian
|
matrix:
|
||||||
platform: linux/amd64
|
distro: [debian, alpine]
|
||||||
runs-on: ubuntu-24.04
|
platform: [linux/amd64, linux/arm64, linux/arm/v7]
|
||||||
push: false
|
pio_env: [native, native-tft]
|
||||||
|
exclude:
|
||||||
docker-deb-amd64-tft:
|
- distro: alpine
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
pio_env: native-tft
|
|
||||||
|
|
||||||
docker-alp-amd64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: alpine
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
|
|
||||||
docker-alp-amd64-tft:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: alpine
|
|
||||||
platform: linux/amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
push: false
|
|
||||||
pio_env: native-tft
|
|
||||||
|
|
||||||
docker-deb-arm64:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/arm64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
push: false
|
|
||||||
|
|
||||||
docker-deb-armv7:
|
|
||||||
uses: ./.github/workflows/docker_build.yml
|
|
||||||
with:
|
|
||||||
distro: debian
|
|
||||||
platform: linux/arm/v7
|
platform: linux/arm/v7
|
||||||
runs-on: ubuntu-24.04-arm
|
- pio_env: native-tft
|
||||||
|
platform: linux/arm64
|
||||||
|
- pio_env: native-tft
|
||||||
|
platform: linux/arm/v7
|
||||||
|
uses: ./.github/workflows/docker_build.yml
|
||||||
|
with:
|
||||||
|
distro: ${{ matrix.distro }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||||
|
pio_env: ${{ matrix.pio_env }}
|
||||||
push: false
|
push: false
|
||||||
|
|
||||||
gather-artifacts:
|
gather-artifacts:
|
||||||
@@ -260,26 +139,15 @@ jobs:
|
|||||||
- rp2350
|
- rp2350
|
||||||
- stm32
|
- stm32
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs: [version, build]
|
||||||
[
|
|
||||||
version,
|
|
||||||
build-esp32,
|
|
||||||
build-esp32s3,
|
|
||||||
build-esp32c3,
|
|
||||||
build-esp32c6,
|
|
||||||
build-nrf52840,
|
|
||||||
build-rp2040,
|
|
||||||
build-rp2350,
|
|
||||||
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}}-*
|
||||||
@@ -308,7 +176,7 @@ jobs:
|
|||||||
./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}}-${{ needs.version.outputs.long }}
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -353,10 +221,10 @@ jobs:
|
|||||||
- 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@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
@@ -372,14 +240,14 @@ jobs:
|
|||||||
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-${{ needs.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-${{ needs.version.outputs.long }}
|
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -422,14 +290,14 @@ jobs:
|
|||||||
needs: [release-artifacts, version]
|
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@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -446,7 +314,7 @@ jobs:
|
|||||||
- name: Zip firmware
|
- name: Zip firmware
|
||||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.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}}-${{ needs.version.outputs.long }}.zip
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -477,14 +345,14 @@ jobs:
|
|||||||
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
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@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
2
.github/workflows/pr_enforce_labels.yml
vendored
2
.github/workflows/pr_enforce_labels.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-label:
|
check-label:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check for PR labels
|
- name: Check for PR labels
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
|
|||||||
6
.github/workflows/release_channels.yml
vendored
6
.github/workflows/release_channels.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
series:
|
series:
|
||||||
- jammy # 22.04
|
- jammy # 22.04 LTS
|
||||||
- noble # 24.04
|
- noble # 24.04 LTS
|
||||||
- plucky # 25.04
|
- plucky # 25.04
|
||||||
# - questing # 25.10
|
- questing # 25.10
|
||||||
uses: ./.github/workflows/package_ppa.yml
|
uses: ./.github/workflows/package_ppa.yml
|
||||||
with:
|
with:
|
||||||
ppa_repo: |-
|
ppa_repo: |-
|
||||||
|
|||||||
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
2
.github/workflows/sec_sast_semgrep_cron.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
# step 4
|
# step 4
|
||||||
- name: publish code scanning alerts
|
- name: publish code scanning alerts
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
with:
|
with:
|
||||||
sarif_file: report.sarif
|
sarif_file: report.sarif
|
||||||
category: semgrep
|
category: semgrep
|
||||||
|
|||||||
4
.github/workflows/stale_bot.yml
vendored
4
.github/workflows/stale_bot.yml
vendored
@@ -17,8 +17,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Stale PR+Issues
|
- name: Stale PR+Issues
|
||||||
uses: actions/stale@v10.0.0
|
uses: actions/stale@v10.1.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 45
|
days-before-stale: 45
|
||||||
|
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.
|
||||||
|
close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened.
|
||||||
exempt-issue-labels: pinned,3.0
|
exempt-issue-labels: pinned,3.0
|
||||||
exempt-pr-labels: pinned,3.0
|
exempt-pr-labels: pinned,3.0
|
||||||
|
|||||||
2
.github/workflows/test_native.yml
vendored
2
.github/workflows/test_native.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Integration test
|
- name: Integration test
|
||||||
run: |
|
run: |
|
||||||
.pio/build/coverage/program &
|
.pio/build/coverage/program -s &
|
||||||
PID=$!
|
PID=$!
|
||||||
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
|
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
|
||||||
echo "Simulator started, launching python test..."
|
echo "Simulator started, launching python test..."
|
||||||
|
|||||||
@@ -8,25 +8,25 @@ plugins:
|
|||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.471
|
- checkov@3.2.473
|
||||||
- renovate@41.115.2
|
- renovate@41.132.5
|
||||||
- prettier@3.6.2
|
- prettier@3.6.2
|
||||||
- trufflehog@3.90.6
|
- trufflehog@3.90.8
|
||||||
- yamllint@1.37.1
|
- yamllint@1.37.1
|
||||||
- bandit@1.8.6
|
- bandit@1.8.6
|
||||||
- trivy@0.66.0
|
- trivy@0.67.0
|
||||||
- taplo@0.10.0
|
- taplo@0.10.0
|
||||||
- ruff@0.13.0
|
- ruff@0.13.3
|
||||||
- isort@6.0.1
|
- isort@6.1.0
|
||||||
- markdownlint@0.45.0
|
- markdownlint@0.45.0
|
||||||
- oxipng@9.1.5
|
- oxipng@9.1.5
|
||||||
- svgo@4.0.0
|
- svgo@4.0.0
|
||||||
- actionlint@1.7.7
|
- actionlint@1.7.7
|
||||||
- flake8@7.3.0
|
- flake8@7.3.0
|
||||||
- hadolint@2.13.1
|
- hadolint@2.14.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- shellcheck@0.11.0
|
- shellcheck@0.11.0
|
||||||
- black@25.1.0
|
- black@25.9.0
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gitleaks@8.28.0
|
- gitleaks@8.28.0
|
||||||
- clang-format@16.0.3
|
- clang-format@16.0.3
|
||||||
|
|||||||
@@ -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-slim-trixie AS builder
|
FROM python:3.14-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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
# trunk-ignore-all(hadolint/DL3018): Do not pin apk 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-alpine3.22 AS builder
|
FROM python:3.14-alpine3.22 AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ build_flags =
|
|||||||
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
|
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
|
||||||
-DAXP_DEBUG_PORT=Serial
|
-DAXP_DEBUG_PORT=Serial
|
||||||
-DCONFIG_BT_NIMBLE_ENABLED
|
-DCONFIG_BT_NIMBLE_ENABLED
|
||||||
|
-DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3
|
||||||
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=2
|
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=2
|
||||||
-DCONFIG_BT_NIMBLE_MAX_CCCDS=20
|
-DCONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||||
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
|
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
|
||||||
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
|
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
|
||||||
-DSERIAL_BUFFER_SIZE=4096
|
-DSERIAL_BUFFER_SIZE=4096
|
||||||
|
-DSERIAL_HAS_ON_RECEIVE
|
||||||
-DLIBPAX_ARDUINO
|
-DLIBPAX_ARDUINO
|
||||||
-DLIBPAX_WIFI
|
-DLIBPAX_WIFI
|
||||||
-DLIBPAX_BLE
|
-DLIBPAX_BLE
|
||||||
@@ -55,7 +57,7 @@ lib_deps =
|
|||||||
# 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=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
|
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
|
||||||
https://github.com/lewisxhe/XPowersLib/archive/v0.3.0.zip
|
https://github.com/lewisxhe/XPowersLib/archive/v0.3.1.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.3.0
|
lewisxhe/XPowersLib@0.3.1
|
||||||
# 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
|
||||||
|
|||||||
@@ -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/c490bcd019e0658404088a61b96e653c9da22c45.zip
|
https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
|
||||||
BPS_RESET=false
|
BPS_RESET=false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
|
||||||
CHANGE_MODE=false
|
CHANGE_MODE=false
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""Generate the CI matrix."""
|
"""Generate the CI matrix."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
from platformio.project.config import ProjectConfig
|
from platformio.project.config import ProjectConfig
|
||||||
|
|
||||||
options = sys.argv[1:]
|
parser = argparse.ArgumentParser(description="Generate the CI matrix")
|
||||||
|
parser.add_argument("platform", help="Platform to build for")
|
||||||
|
parser.add_argument(
|
||||||
|
"--level",
|
||||||
|
choices=["extra", "pr"],
|
||||||
|
nargs="*",
|
||||||
|
default=[],
|
||||||
|
help="Board level to build for (omit for full release boards)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
outlist = []
|
outlist = []
|
||||||
|
|
||||||
if len(options) < 1:
|
|
||||||
print(json.dumps(outlist))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
cfg = ProjectConfig.get_instance()
|
cfg = ProjectConfig.get_instance()
|
||||||
pio_envs = cfg.envs()
|
pio_envs = cfg.envs()
|
||||||
|
|
||||||
# Gather all PlatformIO environments for filtering later
|
# Gather all PlatformIO environments for filtering later
|
||||||
all_envs = []
|
all_envs = []
|
||||||
for pio_env in pio_envs:
|
for pio_env in pio_envs:
|
||||||
env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags')
|
env_build_flags = cfg.get(f"env:{pio_env}", "build_flags")
|
||||||
env_platform = None
|
env_platform = None
|
||||||
for flag in env_build_flags:
|
for flag in env_build_flags:
|
||||||
# Extract the platform from the build flags
|
# Extract the platform from the build flags
|
||||||
@@ -37,36 +41,35 @@ for pio_env in pio_envs:
|
|||||||
exit(1)
|
exit(1)
|
||||||
# Store env details as a dictionary, and add to 'all_envs' list
|
# Store env details as a dictionary, and add to 'all_envs' list
|
||||||
env = {
|
env = {
|
||||||
'name': pio_env,
|
"ci": {"board": pio_env, "platform": env_platform},
|
||||||
'platform': env_platform,
|
"board_level": cfg.get(f"env:{pio_env}", "board_level", default=None),
|
||||||
'board_level': cfg.get(f"env:{pio_env}", 'board_level', default=None),
|
"board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)),
|
||||||
'board_check': bool(cfg.get(f"env:{pio_env}", 'board_check', default=False))
|
|
||||||
}
|
}
|
||||||
all_envs.append(env)
|
all_envs.append(env)
|
||||||
|
|
||||||
# Filter outputs based on options
|
# Filter outputs based on options
|
||||||
# Check is mutually exclusive with other options (except 'pr')
|
# Check is mutually exclusive with other options (except 'pr')
|
||||||
if "check" in options:
|
if "check" in args.platform:
|
||||||
for env in all_envs:
|
for env in all_envs:
|
||||||
if env['board_check']:
|
if env["board_check"]:
|
||||||
if "pr" in options:
|
if "pr" in args.level:
|
||||||
if env['board_level'] == 'pr':
|
if env["board_level"] == "pr":
|
||||||
outlist.append(env['name'])
|
outlist.append(env["ci"])
|
||||||
else:
|
else:
|
||||||
outlist.append(env['name'])
|
outlist.append(env["ci"])
|
||||||
# Filter (non-check) builds by platform
|
# Filter (non-check) builds by platform
|
||||||
else:
|
else:
|
||||||
for env in all_envs:
|
for env in all_envs:
|
||||||
if options[0] == env['platform']:
|
if args.platform == env["ci"]["platform"] or args.platform == "all":
|
||||||
# Always include board_level = 'pr'
|
# Always include board_level = 'pr'
|
||||||
if env['board_level'] == 'pr':
|
if env["board_level"] == "pr":
|
||||||
outlist.append(env['name'])
|
outlist.append(env["ci"])
|
||||||
# Include board_level = 'extra' when requested
|
# Include board_level = 'extra' when requested
|
||||||
elif "extra" in options and env['board_level'] == "extra":
|
elif "extra" in args.level and env["board_level"] == "extra":
|
||||||
outlist.append(env['name'])
|
outlist.append(env["ci"])
|
||||||
# If no board level is specified, include in release builds (not PR)
|
# If no board level is specified, include in release builds (not PR)
|
||||||
elif "pr" not in options and not env['board_level']:
|
elif "pr" not in args.level and not env["board_level"]:
|
||||||
outlist.append(env['name'])
|
outlist.append(env["ci"])
|
||||||
|
|
||||||
# Return as a JSON list
|
# Return as a JSON list
|
||||||
print(json.dumps(outlist))
|
print(json.dumps(outlist))
|
||||||
|
|||||||
116
bin/kill-github-actions.sh
Executable file
116
bin/kill-github-actions.sh
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to cancel all running GitHub Actions workflows
|
||||||
|
# Requires GitHub CLI (gh) to be installed and authenticated
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if gh CLI is installed
|
||||||
|
if ! command -v gh &> /dev/null; then
|
||||||
|
print_error "GitHub CLI (gh) is not installed. Please install it first:"
|
||||||
|
echo " brew install gh"
|
||||||
|
echo " Or visit: https://cli.github.com/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if authenticated
|
||||||
|
if ! gh auth status &> /dev/null; then
|
||||||
|
print_error "GitHub CLI is not authenticated. Please run:"
|
||||||
|
echo " gh auth login"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get repository info
|
||||||
|
REPO=$(gh repo view --json owner,name -q '.owner.login + "/" + .name')
|
||||||
|
if [[ -z "$REPO" ]]; then
|
||||||
|
print_error "Could not determine repository. Make sure you're in a GitHub repository."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Working with repository: $REPO"
|
||||||
|
|
||||||
|
# Get all active workflows (both queued and in-progress)
|
||||||
|
print_status "Fetching active workflows (queued and in-progress)..."
|
||||||
|
QUEUED_WORKFLOWS=$(gh run list --status queued --json databaseId,displayTitle,headBranch,status --limit 100)
|
||||||
|
IN_PROGRESS_WORKFLOWS=$(gh run list --status in_progress --json databaseId,displayTitle,headBranch,status --limit 100)
|
||||||
|
|
||||||
|
# Combine both lists
|
||||||
|
ALL_WORKFLOWS=$(echo "$QUEUED_WORKFLOWS $IN_PROGRESS_WORKFLOWS" | jq -s 'add | unique_by(.databaseId)')
|
||||||
|
|
||||||
|
if [[ "$ALL_WORKFLOWS" == "[]" ]]; then
|
||||||
|
print_status "No active workflows found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse and display active workflows
|
||||||
|
echo
|
||||||
|
print_warning "Found active workflows:"
|
||||||
|
echo "$ALL_WORKFLOWS" | jq -r '.[] | " - \(.displayTitle) (Branch: \(.headBranch), Status: \(.status), ID: \(.databaseId))"'
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -p "Do you want to cancel ALL these workflows? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
print_status "Cancelled by user."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cancel each workflow
|
||||||
|
print_status "Cancelling workflows..."
|
||||||
|
CANCELLED_COUNT=0
|
||||||
|
FAILED_COUNT=0
|
||||||
|
|
||||||
|
while IFS= read -r WORKFLOW_ID; do
|
||||||
|
if [[ -n "$WORKFLOW_ID" ]]; then
|
||||||
|
print_status "Cancelling workflow ID: $WORKFLOW_ID"
|
||||||
|
if gh run cancel "$WORKFLOW_ID" 2>/dev/null; then
|
||||||
|
((CANCELLED_COUNT++))
|
||||||
|
else
|
||||||
|
print_error "Failed to cancel workflow ID: $WORKFLOW_ID"
|
||||||
|
((FAILED_COUNT++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(echo "$ALL_WORKFLOWS" | jq -r '.[].databaseId')
|
||||||
|
|
||||||
|
echo
|
||||||
|
print_status "Summary:"
|
||||||
|
echo " - Cancelled: $CANCELLED_COUNT workflows"
|
||||||
|
if [[ $FAILED_COUNT -gt 0 ]]; then
|
||||||
|
echo " - Failed: $FAILED_COUNT workflows"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "Done!"
|
||||||
|
|
||||||
|
# Optional: Show remaining active workflows
|
||||||
|
echo
|
||||||
|
print_status "Checking for any remaining active workflows..."
|
||||||
|
REMAINING_QUEUED=$(gh run list --status queued --json databaseId --limit 10)
|
||||||
|
REMAINING_IN_PROGRESS=$(gh run list --status in_progress --json databaseId --limit 10)
|
||||||
|
REMAINING_ALL=$(echo "$REMAINING_QUEUED $REMAINING_IN_PROGRESS" | jq -s 'add | unique_by(.databaseId)')
|
||||||
|
|
||||||
|
if [[ "$REMAINING_ALL" == "[]" ]]; then
|
||||||
|
print_status "All workflows successfully cancelled."
|
||||||
|
else
|
||||||
|
REMAINING_COUNT=$(echo "$REMAINING_ALL" | jq '. | length')
|
||||||
|
print_warning "Still $REMAINING_COUNT workflows active (may take a moment to update status)"
|
||||||
|
fi
|
||||||
@@ -87,6 +87,18 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.7.13" date="2025-10-11">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.13</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.12" date="2025-10-01">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.11" date="2025-09-24">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11</url>
|
||||||
|
</release>
|
||||||
|
<release version="2.7.10" date="2025-09-18">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10</url>
|
||||||
|
</release>
|
||||||
<release version="2.7.9" date="2025-09-03">
|
<release version="2.7.9" date="2025-09-03">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ if platform.name == "espressif32":
|
|||||||
|
|
||||||
if platform.name == "nordicnrf52":
|
if platform.name == "nordicnrf52":
|
||||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
|
||||||
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py $BUILD_DIR/firmware.hex -c -f 0xADA52840 -o $BUILD_DIR/firmware.uf2",
|
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"",
|
||||||
"Generating UF2 file"))
|
"Generating UF2 file"))
|
||||||
|
|
||||||
Import("projenv")
|
Import("projenv")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.6.4
|
2.6.6
|
||||||
43
boards/heltec_v4.json
Normal file
43
boards/heltec_v4.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"partitions": "default_16MB.csv",
|
||||||
|
"memory_type": "qio_qspi"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=0",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"psram_type": "qspi",
|
||||||
|
"hwids": [["0x303A", "0x1001"]],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "heltec_v4"
|
||||||
|
},
|
||||||
|
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||||
|
"debug": {
|
||||||
|
"default_tool": "esp-builtin",
|
||||||
|
"onboard_tools": ["esp-builtin"],
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "espidf"],
|
||||||
|
"name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 2097152,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"wait_for_upload_port": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"url": "https://heltec.org/",
|
||||||
|
"vendor": "heltec"
|
||||||
|
}
|
||||||
37
boards/heltec_wireless_tracker_v2.json
Normal file
37
boards/heltec_wireless_tracker_v2.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"partitions": "default_8MB.csv"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=0",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"hwids": [["0x303A", "0x1001"]],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "heltec_wireless_tracker_v2"
|
||||||
|
},
|
||||||
|
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": ["arduino", "espidf"],
|
||||||
|
"name": "Heltec Wireless Tracker V2",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "8MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 8388608,
|
||||||
|
"wait_for_upload_port": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"url": "https://heltec.org",
|
||||||
|
"vendor": "Heltec"
|
||||||
|
}
|
||||||
52
boards/r1-neo.json
Normal file
52
boards/r1-neo.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": "Muzi R1 Neo",
|
||||||
|
"mcu": "nrf52840",
|
||||||
|
"variant": "r1-neo",
|
||||||
|
"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": "WisCore RAK4631 Board",
|
||||||
|
"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://muzi.works/",
|
||||||
|
"vendor": "Muzi Works"
|
||||||
|
}
|
||||||
53
debian/changelog
vendored
53
debian/changelog
vendored
@@ -1,50 +1,19 @@
|
|||||||
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
|
meshtasticd (2.7.13.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Version 2.7.13
|
||||||
|
|
||||||
|
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 11 Oct 2025 15:27:28 +0000
|
||||||
|
|
||||||
|
meshtasticd (2.7.12.0) unstable; urgency=medium
|
||||||
|
|
||||||
[ Austin Lane ]
|
[ Austin Lane ]
|
||||||
* Initial packaging
|
* Initial packaging
|
||||||
* GitHub Actions Automatic version bump
|
* Version 2.5.19
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
[ ]
|
||||||
* GitHub Actions Automatic version bump
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
[ ]
|
[ GitHub Actions ]
|
||||||
* GitHub Actions Automatic version bump
|
* Version 2.7.12
|
||||||
|
|
||||||
[ ]
|
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 01 Oct 2025 19:51:41 +0000
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* 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 Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
* GitHub Actions Automatic version bump
|
|
||||||
|
|
||||||
-- <github-actions[bot]@users.noreply.github.com> Wed, 03 Sep 2025 23:39:17 +0000
|
|
||||||
|
|||||||
5
debian/ci_changelog.sh
vendored
5
debian/ci_changelog.sh
vendored
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
export DEBFULLNAME="GitHub Actions"
|
||||||
export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
|
export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
|
||||||
PKG_VERSION=$(python3 bin/buildinfo.py short)
|
PKG_VERSION=$(python3 bin/buildinfo.py short)
|
||||||
|
|
||||||
dch --newversion "$PKG_VERSION.0" \
|
dch --newversion "$PKG_VERSION.0" \
|
||||||
--distribution UNRELEASED \
|
--distribution unstable \
|
||||||
"GitHub Actions Automatic version bump"
|
"Version $PKG_VERSION"
|
||||||
|
|||||||
2
debian/meshtasticd.postinst
vendored
2
debian/meshtasticd.postinst
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
# 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/bash
|
#!/bin/sh
|
||||||
# postrm script for meshtasticd
|
# postrm script for meshtasticd
|
||||||
#
|
#
|
||||||
# see: dh_installdeb(1)
|
# see: dh_installdeb(1)
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-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 ; set in platformio-custom.py now
|
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
|
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
|
||||||
|
#-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs
|
||||||
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
@@ -68,7 +70,7 @@ lib_deps =
|
|||||||
# 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
|
||||||
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
|
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
|
||||||
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
|
||||||
https://github.com/meshtastic/ArduinoThread/archive/7c3ee9e1951551b949763b1f5280f8db1fa4068d.zip
|
https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip
|
||||||
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
|
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
|
||||||
nanopb/Nanopb@0.4.91
|
nanopb/Nanopb@0.4.91
|
||||||
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
|
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
|
||||||
@@ -113,18 +115,18 @@ lib_deps =
|
|||||||
[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.1
|
jgromes/RadioLib@7.3.0
|
||||||
|
|
||||||
[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/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip
|
https://github.com/meshtastic/device-ui/archive/3fb7c0e28e8e51fc0a7d56facacf3411f1d29fe0.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.2
|
adafruit/Adafruit BusIO@1.17.4
|
||||||
# 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
|
||||||
|
|||||||
Submodule protobufs updated: 945b796a98...38638f19f8
@@ -8,6 +8,7 @@
|
|||||||
"replacements:all",
|
"replacements:all",
|
||||||
"workarounds:all"
|
"workarounds:all"
|
||||||
],
|
],
|
||||||
|
"baseBranchPatterns": ["master"],
|
||||||
"forkProcessing": "enabled",
|
"forkProcessing": "enabled",
|
||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"protobufs"
|
"protobufs"
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
#include <AudioOutputI2S.h>
|
#include <AudioOutputI2S.h>
|
||||||
#include <ESP8266SAM.h>
|
#include <ESP8266SAM.h>
|
||||||
|
|
||||||
|
#ifdef USE_XL9555
|
||||||
|
#include "ExtensionIOXL9555.hpp"
|
||||||
|
extern ExtensionIOXL9555 io;
|
||||||
|
#endif
|
||||||
|
|
||||||
#define AUDIO_THREAD_INTERVAL_MS 100
|
#define AUDIO_THREAD_INTERVAL_MS 100
|
||||||
|
|
||||||
class AudioThread : public concurrency::OSThread
|
class AudioThread : public concurrency::OSThread
|
||||||
@@ -20,12 +25,16 @@ class AudioThread : public concurrency::OSThread
|
|||||||
|
|
||||||
void beginRttl(const void *data, uint32_t len)
|
void beginRttl(const void *data, uint32_t len)
|
||||||
{
|
{
|
||||||
|
#ifdef T_LORA_PAGER
|
||||||
|
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||||
|
#endif
|
||||||
setCPUFast(true);
|
setCPUFast(true);
|
||||||
rtttlFile = new AudioFileSourcePROGMEM(data, len);
|
rtttlFile = new AudioFileSourcePROGMEM(data, len);
|
||||||
i2sRtttl = new AudioGeneratorRTTTL();
|
i2sRtttl = new AudioGeneratorRTTTL();
|
||||||
i2sRtttl->begin(rtttlFile, audioOut);
|
i2sRtttl->begin(rtttlFile, audioOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also handles actually playing the RTTTL, needs to be called in loop
|
||||||
bool isPlaying()
|
bool isPlaying()
|
||||||
{
|
{
|
||||||
if (i2sRtttl != nullptr) {
|
if (i2sRtttl != nullptr) {
|
||||||
@@ -45,6 +54,9 @@ class AudioThread : public concurrency::OSThread
|
|||||||
rtttlFile = nullptr;
|
rtttlFile = nullptr;
|
||||||
|
|
||||||
setCPUFast(false);
|
setCPUFast(false);
|
||||||
|
#ifdef T_LORA_PAGER
|
||||||
|
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void readAloud(const char *text)
|
void readAloud(const char *text)
|
||||||
@@ -55,10 +67,16 @@ class AudioThread : public concurrency::OSThread
|
|||||||
i2sRtttl = nullptr;
|
i2sRtttl = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef T_LORA_PAGER
|
||||||
|
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
|
||||||
|
#endif
|
||||||
ESP8266SAM *sam = new ESP8266SAM;
|
ESP8266SAM *sam = new ESP8266SAM;
|
||||||
sam->Say(audioOut, text);
|
sam->Say(audioOut, text);
|
||||||
delete sam;
|
delete sam;
|
||||||
setCPUFast(false);
|
setCPUFast(false);
|
||||||
|
#ifdef T_LORA_PAGER
|
||||||
|
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
|
|||||||
{
|
{
|
||||||
int result;
|
int result;
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
bool utf = !settingsMap[ascii_logs];
|
bool utf = !portduino_config.ascii_logs;
|
||||||
#else
|
#else
|
||||||
bool utf = true;
|
bool utf = true;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -39,3 +39,45 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_CLIENT:
|
||||||
|
return "Client";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
|
||||||
|
return "Client Mute";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
|
||||||
|
return "Client Hidden";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE:
|
||||||
|
return "Client Base";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
|
||||||
|
return "Lost and Found";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_TRACKER:
|
||||||
|
return "Tracker";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_SENSOR:
|
||||||
|
return "Sensor";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_TAK:
|
||||||
|
return "TAK";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
|
||||||
|
return "TAK Tracker";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_ROUTER:
|
||||||
|
return "Router";
|
||||||
|
break;
|
||||||
|
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
|
||||||
|
return "Router Late";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ class DisplayFormatters
|
|||||||
public:
|
public:
|
||||||
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
||||||
bool usePreset);
|
bool usePreset);
|
||||||
|
static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class GPSStatus : public Status
|
|||||||
|
|
||||||
meshtastic_Position p = meshtastic_Position_init_default;
|
meshtastic_Position p = meshtastic_Position_init_default;
|
||||||
|
|
||||||
|
/// Time of last valid GPS fix (millis since boot)
|
||||||
|
uint32_t lastFixMillis = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GPSStatus() { statusType = STATUS_TYPE_GPS; }
|
GPSStatus() { statusType = STATUS_TYPE_GPS; }
|
||||||
|
|
||||||
@@ -83,6 +86,9 @@ class GPSStatus : public Status
|
|||||||
|
|
||||||
uint32_t getNumSatellites() const { return p.sats_in_view; }
|
uint32_t getNumSatellites() const { return p.sats_in_view; }
|
||||||
|
|
||||||
|
/// Return millis() when the last GPS fix occurred (0 = never)
|
||||||
|
uint32_t getLastFixMillis() const { return lastFixMillis; }
|
||||||
|
|
||||||
bool matches(const GPSStatus *newStatus) const
|
bool matches(const GPSStatus *newStatus) const
|
||||||
{
|
{
|
||||||
#ifdef GPS_DEBUG
|
#ifdef GPS_DEBUG
|
||||||
@@ -114,6 +120,9 @@ class GPSStatus : public Status
|
|||||||
|
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
if (hasLock) {
|
if (hasLock) {
|
||||||
|
// Record time of last valid GPS fix
|
||||||
|
lastFixMillis = millis();
|
||||||
|
|
||||||
// In debug logs, identify position by @timestamp:stage (stage 3 = notify)
|
// In debug logs, identify position by @timestamp:stage (stage 3 = notify)
|
||||||
LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp,
|
LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp,
|
||||||
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,
|
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
|||||||
config.power.device_battery_ina_address) {
|
config.power.device_battery_ina_address) {
|
||||||
if (!ina226Sensor.isInitialized())
|
if (!ina226Sensor.isInitialized())
|
||||||
return ina226Sensor.runOnce() > 0;
|
return ina226Sensor.runOnce() > 0;
|
||||||
|
return ina226Sensor.isRunning();
|
||||||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
|
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
|
||||||
config.power.device_battery_ina_address) {
|
config.power.device_battery_ina_address) {
|
||||||
if (!ina260Sensor.isInitialized())
|
if (!ina260Sensor.isInitialized())
|
||||||
@@ -691,6 +692,16 @@ bool Power::setup()
|
|||||||
#ifdef NRF_APM
|
#ifdef NRF_APM
|
||||||
found = true;
|
found = true;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef EXT_PWR_DETECT
|
||||||
|
attachInterrupt(
|
||||||
|
EXT_PWR_DETECT,
|
||||||
|
[]() {
|
||||||
|
power->setIntervalFromNow(0);
|
||||||
|
runASAP = true;
|
||||||
|
BaseType_t higherWake = 0;
|
||||||
|
},
|
||||||
|
CHANGE);
|
||||||
|
#endif
|
||||||
|
|
||||||
enabled = found;
|
enabled = found;
|
||||||
low_voltage_counter = 0;
|
low_voltage_counter = 0;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "memGet.h"
|
||||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -57,7 +58,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
bool color = !settingsMap[ascii_logs];
|
bool color = !portduino_config.ascii_logs;
|
||||||
#else
|
#else
|
||||||
bool color = true;
|
bool color = true;
|
||||||
#endif
|
#endif
|
||||||
@@ -99,7 +100,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
|||||||
size_t r = 0;
|
size_t r = 0;
|
||||||
|
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
bool color = !settingsMap[ascii_logs];
|
bool color = !portduino_config.ascii_logs;
|
||||||
#else
|
#else
|
||||||
bool color = true;
|
bool color = true;
|
||||||
#endif
|
#endif
|
||||||
@@ -166,6 +167,16 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
|
|||||||
print(thread->ThreadName);
|
print(thread->ThreadName);
|
||||||
print("] ");
|
print("] ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_HEAP
|
||||||
|
// Add heap free space bytes prefix before every log message
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
::printf("[heap %u] ", memGet.getFreeHeap());
|
||||||
|
#else
|
||||||
|
printf("[heap %u] ", memGet.getFreeHeap());
|
||||||
|
#endif
|
||||||
|
#endif // DEBUG_HEAP
|
||||||
|
|
||||||
r += vprintf(logLevel, format, arg);
|
r += vprintf(logLevel, format, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +299,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
|||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
// level trace is special, two possible ways to handle it.
|
// level trace is special, two possible ways to handle it.
|
||||||
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||||
if (settingsStrings[traceFilename] != "") {
|
if (portduino_config.traceFilename != "") {
|
||||||
va_list arg;
|
va_list arg;
|
||||||
va_start(arg, format);
|
va_start(arg, format);
|
||||||
try {
|
try {
|
||||||
@@ -297,18 +308,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
|||||||
}
|
}
|
||||||
va_end(arg);
|
va_end(arg);
|
||||||
}
|
}
|
||||||
if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
|
||||||
delete[] newFormat;
|
delete[] newFormat;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
|
||||||
delete[] newFormat;
|
delete[] newFormat;
|
||||||
return;
|
return;
|
||||||
} else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
|
||||||
delete[] newFormat;
|
delete[] newFormat;
|
||||||
return;
|
return;
|
||||||
} else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
|
||||||
delete[] newFormat;
|
delete[] newFormat;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "time.h"
|
#include "time.h"
|
||||||
|
|
||||||
|
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||||
|
#define IS_USB_SERIAL
|
||||||
|
#ifdef SERIAL_HAS_ON_RECEIVE
|
||||||
|
#undef SERIAL_HAS_ON_RECEIVE
|
||||||
|
#endif
|
||||||
|
#include "HWCDC.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef RP2040_SLOW_CLOCK
|
#ifdef RP2040_SLOW_CLOCK
|
||||||
#define Port Serial2
|
#define Port Serial2
|
||||||
#else
|
#else
|
||||||
@@ -22,7 +30,12 @@ SerialConsole *console;
|
|||||||
|
|
||||||
void consoleInit()
|
void consoleInit()
|
||||||
{
|
{
|
||||||
new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
||||||
|
|
||||||
|
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||||
|
// onReceive does only exist for HardwareSerial not for USB CDC serial
|
||||||
|
Port.onReceive([sc]() { sc->rxInt(); });
|
||||||
|
#endif
|
||||||
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
DEBUG_PORT.rpInit(); // Simply sets up semaphore
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,14 +78,21 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
|||||||
int32_t SerialConsole::runOnce()
|
int32_t SerialConsole::runOnce()
|
||||||
{
|
{
|
||||||
#ifdef HELTEC_MESH_SOLAR
|
#ifdef HELTEC_MESH_SOLAR
|
||||||
//After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
|
||||||
if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port
|
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
|
||||||
&& moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)
|
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) {
|
||||||
{
|
|
||||||
return 250;
|
return 250;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return runOncePart();
|
|
||||||
|
int32_t delay = runOncePart();
|
||||||
|
#if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||||
|
return Port.available() ? delay : INT32_MAX;
|
||||||
|
#elif defined(IS_USB_SERIAL)
|
||||||
|
return HWCDC::isPlugged() ? delay : (1000 * 20);
|
||||||
|
#else
|
||||||
|
return delay;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void SerialConsole::flush()
|
void SerialConsole::flush()
|
||||||
@@ -80,6 +100,18 @@ void SerialConsole::flush()
|
|||||||
Port.flush();
|
Port.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger tx of serial data
|
||||||
|
void SerialConsole::onNowHasData(uint32_t fromRadioNum)
|
||||||
|
{
|
||||||
|
setIntervalFromNow(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger rx of serial data
|
||||||
|
void SerialConsole::rxInt()
|
||||||
|
{
|
||||||
|
setIntervalFromNow(0);
|
||||||
|
}
|
||||||
|
|
||||||
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
|
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
|
||||||
bool SerialConsole::checkIsConnected()
|
bool SerialConsole::checkIsConnected()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,11 +32,14 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
|||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
void flush();
|
void flush();
|
||||||
|
void rxInt();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Check the current underlying physical link to see if the client is currently connected
|
/// Check the current underlying physical link to see if the client is currently connected
|
||||||
virtual bool checkIsConnected() override;
|
virtual bool checkIsConnected() override;
|
||||||
|
|
||||||
|
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
||||||
|
|
||||||
/// Possibly switch to protobufs if we see a valid protobuf message
|
/// Possibly switch to protobufs if we see a valid protobuf message
|
||||||
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
BuzzerFeedbackThread *buzzerFeedbackThread;
|
BuzzerFeedbackThread *buzzerFeedbackThread;
|
||||||
|
|
||||||
BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback")
|
BuzzerFeedbackThread::BuzzerFeedbackThread()
|
||||||
{
|
{
|
||||||
if (inputBroker)
|
if (inputBroker)
|
||||||
inputObserver.observe(inputBroker);
|
inputObserver.observe(inputBroker);
|
||||||
@@ -15,24 +15,24 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
{
|
{
|
||||||
// Only provide feedback if buzzer is enabled for notifications
|
// Only provide feedback if buzzer is enabled for notifications
|
||||||
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
|
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
|
||||||
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) {
|
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY ||
|
||||||
|
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) {
|
||||||
return 0; // Let other handlers process the event
|
return 0; // Let other handlers process the event
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track last event time for potential future use
|
|
||||||
lastEventTime = millis();
|
|
||||||
needsUpdate = true;
|
|
||||||
|
|
||||||
// Handle different input events with appropriate buzzer feedback
|
// Handle different input events with appropriate buzzer feedback
|
||||||
switch (event->inputEvent) {
|
switch (event->inputEvent) {
|
||||||
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
|
||||||
@@ -59,14 +59,3 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
|||||||
|
|
||||||
return 0; // Allow other handlers to process the event
|
return 0; // Allow other handlers to process the event
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t BuzzerFeedbackThread::runOnce()
|
|
||||||
{
|
|
||||||
// This thread is primarily event-driven, but we can use runOnce
|
|
||||||
// for any periodic tasks if needed in the future
|
|
||||||
|
|
||||||
needsUpdate = false;
|
|
||||||
|
|
||||||
// Run every 100ms when active, less frequently when idle
|
|
||||||
return needsUpdate ? 100 : 1000;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "input/InputBroker.h"
|
#include "input/InputBroker.h"
|
||||||
|
|
||||||
class BuzzerFeedbackThread : public concurrency::OSThread
|
class BuzzerFeedbackThread
|
||||||
{
|
{
|
||||||
CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver =
|
CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver =
|
||||||
CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent);
|
CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent);
|
||||||
@@ -12,13 +12,6 @@ class BuzzerFeedbackThread : public concurrency::OSThread
|
|||||||
public:
|
public:
|
||||||
BuzzerFeedbackThread();
|
BuzzerFeedbackThread();
|
||||||
int handleInputEvent(const InputEvent *event);
|
int handleInputEvent(const InputEvent *event);
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual int32_t runOnce() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint32_t lastEventTime = 0;
|
|
||||||
bool needsUpdate = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern BuzzerFeedbackThread *buzzerFeedbackThread;
|
extern BuzzerFeedbackThread *buzzerFeedbackThread;
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ void OSThread::run()
|
|||||||
if (heap < newHeap)
|
if (heap < newHeap)
|
||||||
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef DEBUG_LOOP_TIMING
|
||||||
|
LOG_DEBUG("====== Thread next run in: %d", newDelay);
|
||||||
|
#endif
|
||||||
runned();
|
runned();
|
||||||
|
|
||||||
if (newDelay >= 0)
|
if (newDelay >= 0)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "pcf8563.h"
|
#include "pcf8563.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Offer chance for variant-specific defines */
|
||||||
|
#include "variant.h"
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Version
|
// Version
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -117,6 +120,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#define SX126X_MAX_POWER 22
|
#define SX126X_MAX_POWER 22
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_GC1109_PA
|
||||||
|
// Power Amps are often non-linear, so we can use an array of values for the power curve
|
||||||
|
#define NUM_PA_POINTS 22
|
||||||
|
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef STATION_G2
|
||||||
|
#define NUM_PA_POINTS 19
|
||||||
|
#define TX_GAIN_LORA 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 19, 19, 18, 18
|
||||||
|
#endif
|
||||||
|
|
||||||
// Default system gain to 0 if not defined
|
// Default system gain to 0 if not defined
|
||||||
#ifndef TX_GAIN_LORA
|
#ifndef TX_GAIN_LORA
|
||||||
#define TX_GAIN_LORA 0
|
#define TX_GAIN_LORA 0
|
||||||
@@ -254,14 +268,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
// convert 24-bit color to 16-bit (56K)
|
// convert 24-bit color to 16-bit (56K)
|
||||||
#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
|
#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
|
||||||
|
|
||||||
/* Step #1: offer chance for variant-specific defines */
|
|
||||||
#include "variant.h"
|
|
||||||
|
|
||||||
#if defined(VEXT_ENABLE) && !defined(VEXT_ON_VALUE)
|
#if defined(VEXT_ENABLE) && !defined(VEXT_ON_VALUE)
|
||||||
// Older variant.h files might not be defining this value, so stay with the old default
|
// Older variant.h files might not be defining this value, so stay with the old default
|
||||||
#define VEXT_ON_VALUE LOW
|
#define VEXT_ON_VALUE LOW
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Rotary encoder
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
#ifndef ROTARY_DELAY
|
||||||
|
#define ROTARY_DELAY 5
|
||||||
|
#endif
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// GPS
|
// GPS
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const
|
|||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstRTC() const
|
ScanI2C::FoundDevice ScanI2C::firstRTC() const
|
||||||
{
|
{
|
||||||
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563};
|
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE};
|
||||||
return firstOfOrNONE(2, types);
|
return firstOfOrNONE(3, types);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ScanI2C
|
|||||||
SCREEN_ST7567,
|
SCREEN_ST7567,
|
||||||
RTC_RV3028,
|
RTC_RV3028,
|
||||||
RTC_PCF8563,
|
RTC_PCF8563,
|
||||||
|
RTC_RX8130CE,
|
||||||
CARDKB,
|
CARDKB,
|
||||||
TDECKKB,
|
TDECKKB,
|
||||||
BBQ10KB,
|
BBQ10KB,
|
||||||
|
|||||||
16
src/detect/ScanI2CConsumer.cpp
Normal file
16
src/detect/ScanI2CConsumer.cpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#include "ScanI2CConsumer.h"
|
||||||
|
#include <forward_list>
|
||||||
|
|
||||||
|
static std::forward_list<ScanI2CConsumer *> ScanI2CConsumers;
|
||||||
|
|
||||||
|
ScanI2CConsumer::ScanI2CConsumer()
|
||||||
|
{
|
||||||
|
ScanI2CConsumers.push_front(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanI2CCompleted(ScanI2C *i2cScanner)
|
||||||
|
{
|
||||||
|
for (ScanI2CConsumer *consumer : ScanI2CConsumers) {
|
||||||
|
consumer->i2cScanFinished(i2cScanner);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/detect/ScanI2CConsumer.h
Normal file
13
src/detect/ScanI2CConsumer.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ScanI2C.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
class ScanI2CConsumer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScanI2CConsumer();
|
||||||
|
virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ScanI2CCompleted(ScanI2C *i2cScanner);
|
||||||
@@ -197,6 +197,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
#ifdef PCF8563_RTC
|
#ifdef PCF8563_RTC
|
||||||
SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
|
SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef RX8130CE_RTC
|
||||||
|
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
|
||||||
|
#endif
|
||||||
|
|
||||||
case CARDKB_ADDR:
|
case CARDKB_ADDR:
|
||||||
// Do we have the RAK14006 instead?
|
// Do we have the RAK14006 instead?
|
||||||
@@ -375,7 +378,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
|||||||
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
|
||||||
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
|
||||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
|
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
|
||||||
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) {
|
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0x11f3 || registerValue == 0xe9c ||
|
||||||
|
registerValue == 0xc8d) {
|
||||||
type = SHT4X;
|
type = SHT4X;
|
||||||
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
logFoundDevice("SHT4X", (uint8_t)addr.address);
|
||||||
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
|
||||||
@@ -577,7 +581,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port)
|
|||||||
scanPort(port, nullptr, 0);
|
scanPort(port, nullptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) const
|
TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address)
|
||||||
{
|
{
|
||||||
if (address.port == ScanI2C::I2CPort::WIRE) {
|
if (address.port == ScanI2C::I2CPort::WIRE) {
|
||||||
return &Wire;
|
return &Wire;
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ class ScanI2CTwoWire : public ScanI2C
|
|||||||
|
|
||||||
ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override;
|
ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override;
|
||||||
|
|
||||||
TwoWire *fetchI2CBus(ScanI2C::DeviceAddress) const;
|
|
||||||
|
|
||||||
bool exists(ScanI2C::DeviceType) const override;
|
bool exists(ScanI2C::DeviceType) const override;
|
||||||
|
|
||||||
size_t countDevices() const override;
|
size_t countDevices() const override;
|
||||||
|
|
||||||
|
static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;
|
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;
|
||||||
|
|
||||||
|
|||||||
211
src/gps/GPS.cpp
211
src/gps/GPS.cpp
@@ -494,38 +494,27 @@ bool GPS::setup()
|
|||||||
if (!didSerialInit) {
|
if (!didSerialInit) {
|
||||||
int msglen = 0;
|
int msglen = 0;
|
||||||
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
#ifdef TRACKER_T1000_E
|
|
||||||
// add power up/down strategy, improve ag3335 detection success
|
|
||||||
digitalWrite(PIN_GPS_EN, LOW);
|
|
||||||
delay(500);
|
|
||||||
digitalWrite(GPS_VRTC_EN, LOW);
|
|
||||||
delay(1000);
|
|
||||||
digitalWrite(GPS_VRTC_EN, HIGH);
|
|
||||||
delay(500);
|
|
||||||
digitalWrite(PIN_GPS_EN, HIGH);
|
|
||||||
delay(1000);
|
|
||||||
#endif
|
|
||||||
if (probeTries < GPS_PROBETRIES) {
|
if (probeTries < GPS_PROBETRIES) {
|
||||||
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
|
|
||||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
if (++speedSelect == array_count(serialSpeeds)) {
|
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||||
speedSelect = 0;
|
speedSelect = 0;
|
||||||
++probeTries;
|
++probeTries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Rare Serial Speeds
|
// Rare Serial Speeds
|
||||||
|
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||||
if (probeTries == GPS_PROBETRIES) {
|
if (probeTries == GPS_PROBETRIES) {
|
||||||
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
|
|
||||||
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
||||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
if (++speedSelect == array_count(rareSerialSpeeds)) {
|
if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||||
@@ -807,6 +796,14 @@ bool GPS::setup()
|
|||||||
} else {
|
} else {
|
||||||
LOG_INFO("GNSS module configuration saved!");
|
LOG_INFO("GNSS module configuration saved!");
|
||||||
}
|
}
|
||||||
|
} else if (gnssModel == GNSS_MODEL_CM121) {
|
||||||
|
// only ask for RMC and GGA
|
||||||
|
// enable GGA
|
||||||
|
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
||||||
|
delay(250);
|
||||||
|
// enable RMC
|
||||||
|
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
||||||
|
delay(250);
|
||||||
}
|
}
|
||||||
didSerialInit = true;
|
didSerialInit = true;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1020,7 @@ void GPS::down()
|
|||||||
LOG_DEBUG("%us until next search", sleepTime / 1000);
|
LOG_DEBUG("%us until next search", sleepTime / 1000);
|
||||||
|
|
||||||
// If update interval less than 10 seconds, no attempt to sleep
|
// If update interval less than 10 seconds, no attempt to sleep
|
||||||
if (updateInterval <= 10 * 1000UL || sleepTime == 0)
|
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0)
|
||||||
setPowerState(GPS_IDLE);
|
setPowerState(GPS_IDLE);
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@@ -1084,7 +1081,7 @@ int32_t GPS::runOnce()
|
|||||||
return disable();
|
return disable();
|
||||||
}
|
}
|
||||||
if (!setup())
|
if (!setup())
|
||||||
return 2000; // Setup failed, re-run in two seconds
|
return currentDelay; // Setup failed, re-run in two seconds
|
||||||
|
|
||||||
// We have now loaded our saved preferences from flash
|
// We have now loaded our saved preferences from flash
|
||||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
@@ -1094,11 +1091,29 @@ int32_t GPS::runOnce()
|
|||||||
publishUpdate();
|
publishUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repeaters have no need for GPS
|
// ======================== GPS_ACTIVE state ========================
|
||||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
|
// In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages.
|
||||||
return disable();
|
// We use the following logic to determine when to update the local position
|
||||||
}
|
// or time by running GPS::publishUpdate.
|
||||||
|
// Note: Local position update is asynchronous to position broadcast. We
|
||||||
|
// generally run this state every gps_update_interval seconds, and in most cases
|
||||||
|
// gps_update_interval is faster than the position broadcast interval so there's a
|
||||||
|
// fresh position ready when the device wants to broadcast one on the mesh.
|
||||||
|
//
|
||||||
|
// 1. Got a time for the first time --> set the time, don't publish.
|
||||||
|
// 2. Got a lock for the first time
|
||||||
|
// --> If gps_update_interval is <= 10s --> publishUpdate
|
||||||
|
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
|
||||||
|
// 3. Got a lock after turning back on
|
||||||
|
// --> If gps_update_interval is <= 10s --> publishUpdate
|
||||||
|
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
|
||||||
|
// 4. Hold has expired
|
||||||
|
// --> If we have a time and a location --> publishUpdate
|
||||||
|
// --> down()
|
||||||
|
// 5. Search time has expired
|
||||||
|
// --> If we have a time and a location --> publishUpdate
|
||||||
|
// --> If we had a location before but don't now --> publishUpdate
|
||||||
|
// --> down()
|
||||||
if (whileActive()) {
|
if (whileActive()) {
|
||||||
// if we have received valid NMEA claim we are connected
|
// if we have received valid NMEA claim we are connected
|
||||||
setConnected();
|
setConnected();
|
||||||
@@ -1108,55 +1123,81 @@ int32_t GPS::runOnce()
|
|||||||
if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue())
|
if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue())
|
||||||
up();
|
up();
|
||||||
|
|
||||||
// If we've already set time from the GPS, no need to ask the GPS
|
// quality of the previous fix. We set it to 0 when we go down, so it's a way
|
||||||
|
// to check if we're getting a lock after being GPS_OFF.
|
||||||
|
uint8_t prev_fixQual = fixQual;
|
||||||
|
|
||||||
|
if (powerState == GPS_ACTIVE) {
|
||||||
|
// if gps_update_interval is <=10s, GPS never goes off, so we treat that differently
|
||||||
|
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
|
||||||
|
|
||||||
|
// 1. Got a time for the first time
|
||||||
bool gotTime = (getRTCQuality() >= RTCQualityGPS);
|
bool gotTime = (getRTCQuality() >= RTCQualityGPS);
|
||||||
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
|
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
|
||||||
gotTime = true;
|
gotTime = true;
|
||||||
shouldPublish = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t prev_fixQual = fixQual;
|
// 2. Got a lock for the first time, or 3. Got a lock after turning back on
|
||||||
bool gotLoc = lookForLocation();
|
bool gotLoc = lookForLocation();
|
||||||
if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
|
if (gotLoc) {
|
||||||
|
#ifdef GPS_DEBUG
|
||||||
|
if (!hasValidLocation) { // declare that we have location ASAP
|
||||||
LOG_DEBUG("hasValidLocation RISING EDGE");
|
LOG_DEBUG("hasValidLocation RISING EDGE");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) {
|
||||||
hasValidLocation = true;
|
hasValidLocation = true;
|
||||||
shouldPublish = true;
|
shouldPublish = true;
|
||||||
// Hold for 20secs after getting a lock to download ephemeris etc
|
} else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) {
|
||||||
fixHoldEnds = millis() + 20000;
|
hasValidLocation = true;
|
||||||
}
|
// Hold for up to 20secs after getting a lock to download ephemeris etc
|
||||||
|
uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS;
|
||||||
if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on.
|
if (holdTime > GPS_FIX_HOLD_MAX_MS)
|
||||||
fixHoldEnds = millis() + 20000;
|
holdTime = GPS_FIX_HOLD_MAX_MS;
|
||||||
shouldPublish = true; // Publish immediately, since next publish is at end of hold
|
fixHoldEnds = millis() + holdTime;
|
||||||
}
|
|
||||||
|
|
||||||
bool tooLong = scheduling.searchedTooLong();
|
|
||||||
if (tooLong)
|
|
||||||
LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
|
|
||||||
|
|
||||||
// Once we get a location we no longer desperately want an update
|
|
||||||
if ((gotLoc && gotTime) || tooLong) {
|
|
||||||
if (tooLong && !gotLoc) {
|
|
||||||
// we didn't get a location during this ack window, therefore declare loss of lock
|
|
||||||
if (hasValidLocation) {
|
|
||||||
LOG_DEBUG("hasValidLocation FALLING EDGE");
|
|
||||||
}
|
|
||||||
p = meshtastic_Position_init_default;
|
|
||||||
hasValidLocation = false;
|
|
||||||
}
|
|
||||||
if (millis() > fixHoldEnds) {
|
|
||||||
shouldPublish = true; // publish our update at the end of the lock hold
|
|
||||||
publishUpdate();
|
|
||||||
down();
|
|
||||||
#ifdef GPS_DEBUG
|
#ifdef GPS_DEBUG
|
||||||
} else {
|
LOG_DEBUG("Holding for %ums after lock", holdTime);
|
||||||
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If state has changed do a publish
|
bool tooLong = scheduling.searchedTooLong();
|
||||||
|
if (tooLong && !gotLoc) {
|
||||||
|
LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
|
||||||
|
// we didn't get a location during this ack window, therefore declare loss of lock
|
||||||
|
if (hasValidLocation) {
|
||||||
|
p = meshtastic_Position_init_default;
|
||||||
|
hasValidLocation = false;
|
||||||
|
shouldPublish = true;
|
||||||
|
#ifdef GPS_DEBUG
|
||||||
|
LOG_DEBUG("hasValidLocation FALLING EDGE");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hold has expired , Search time has expired, we got a time only, or we never needed to hold.
|
||||||
|
bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds);
|
||||||
|
if (shouldPublish || tooLong || holdExpired) {
|
||||||
|
if (gotTime && hasValidLocation) {
|
||||||
|
shouldPublish = true;
|
||||||
|
}
|
||||||
|
if (shouldPublish) {
|
||||||
|
fixHoldEnds = 0;
|
||||||
publishUpdate();
|
publishUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's a chance we just got a time, so keep going to see if we can get a location too
|
||||||
|
if (tooLong || holdExpired) {
|
||||||
|
down();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef GPS_DEBUG
|
||||||
|
} else if (fixHoldEnds != 0) {
|
||||||
|
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ===================== end GPS_ACTIVE state ========================
|
||||||
|
|
||||||
if (config.position.fixed_position == true && hasValidLocation)
|
if (config.position.fixed_position == true && hasValidLocation)
|
||||||
return disable(); // This should trigger when we have a fixed position, and get that first position
|
return disable(); // This should trigger when we have a fixed position, and get that first position
|
||||||
@@ -1213,6 +1254,10 @@ static const char *DETECTED_MESSAGE = "%s detected";
|
|||||||
|
|
||||||
GnssModel_t GPS::probe(int serialSpeed)
|
GnssModel_t GPS::probe(int serialSpeed)
|
||||||
{
|
{
|
||||||
|
uint8_t buffer[768] = {0};
|
||||||
|
|
||||||
|
switch (currentStep) {
|
||||||
|
case 0: {
|
||||||
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
||||||
_serial_gps->end();
|
_serial_gps->end();
|
||||||
_serial_gps->begin(serialSpeed);
|
_serial_gps->begin(serialSpeed);
|
||||||
@@ -1222,13 +1267,12 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
_serial_gps->begin(serialSpeed);
|
_serial_gps->begin(serialSpeed);
|
||||||
#else
|
#else
|
||||||
if (_serial_gps->baudRate() != serialSpeed) {
|
if (_serial_gps->baudRate() != serialSpeed) {
|
||||||
LOG_DEBUG("Set Baud to %i", serialSpeed);
|
LOG_DEBUG("Set GPS Baud to %i", serialSpeed);
|
||||||
_serial_gps->updateBaudRate(serialSpeed);
|
_serial_gps->updateBaudRate(serialSpeed);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
memset(&ublox_info, 0, sizeof(ublox_info));
|
memset(&ublox_info, 0, sizeof(ublox_info));
|
||||||
uint8_t buffer[768] = {0};
|
|
||||||
delay(100);
|
delay(100);
|
||||||
|
|
||||||
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
|
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
|
||||||
@@ -1239,17 +1283,35 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
|
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
|
||||||
_serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n");
|
_serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n");
|
||||||
delay(20);
|
delay(20);
|
||||||
|
// Close NMEA sequences on CM121
|
||||||
|
_serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n");
|
||||||
|
_serial_gps->write("$CFGMSG,0,2,0,1*18\r\n");
|
||||||
|
_serial_gps->write("$CFGMSG,0,3,0,1*19\r\n");
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 1;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
|
||||||
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
|
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121
|
||||||
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
|
std::vector<ChipInfo> unicore = {
|
||||||
|
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}};
|
||||||
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
|
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 2;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
std::vector<ChipInfo> atgm = {
|
std::vector<ChipInfo> atgm = {
|
||||||
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
|
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
|
||||||
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
|
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
|
||||||
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
|
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
|
||||||
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
|
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 3;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
|
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
|
||||||
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
|
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
|
||||||
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
|
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
|
||||||
@@ -1258,9 +1320,18 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
|
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
|
||||||
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
|
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
|
||||||
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
|
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 4;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
|
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
|
||||||
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
|
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 5;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
|
||||||
// Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms
|
// Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms
|
||||||
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
|
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
|
||||||
@@ -1271,7 +1342,11 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
{"L80", "_3339_", GNSS_MODEL_MTK_L76B}};
|
{"L80", "_3339_", GNSS_MODEL_MTK_L76B}};
|
||||||
|
|
||||||
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
|
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
|
||||||
|
currentDelay = 20;
|
||||||
|
currentStep = 6;
|
||||||
|
return GNSS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||||
UBXChecksum(cfg_rate, sizeof(cfg_rate));
|
UBXChecksum(cfg_rate, sizeof(cfg_rate));
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
@@ -1280,6 +1355,8 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
GPS_RESPONSE response = getACK(0x06, 0x08, 750);
|
GPS_RESPONSE response = getACK(0x06, 0x08, 750);
|
||||||
if (response == GNSS_RESPONSE_NONE) {
|
if (response == GNSS_RESPONSE_NONE) {
|
||||||
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
|
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
|
||||||
|
currentDelay = 2000;
|
||||||
|
currentStep = 0;
|
||||||
return GNSS_MODEL_UNKNOWN;
|
return GNSS_MODEL_UNKNOWN;
|
||||||
} else if (response == GNSS_RESPONSE_FRAME_ERRORS) {
|
} else if (response == GNSS_RESPONSE_FRAME_ERRORS) {
|
||||||
LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed);
|
LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed);
|
||||||
@@ -1363,7 +1440,11 @@ GnssModel_t GPS::probe(int serialSpeed)
|
|||||||
return GNSS_MODEL_UBLOX10;
|
return GNSS_MODEL_UBLOX10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
|
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
|
||||||
|
currentDelay = 2000;
|
||||||
|
currentStep = 0;
|
||||||
return GNSS_MODEL_UNKNOWN;
|
return GNSS_MODEL_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1440,7 +1521,7 @@ GPS *GPS::createGps()
|
|||||||
_en_gpio = PIN_GPS_EN;
|
_en_gpio = PIN_GPS_EN;
|
||||||
#endif
|
#endif
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
if (!settingsMap[has_gps])
|
if (!portduino_config.has_gps)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
#endif
|
#endif
|
||||||
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
#define GPS_EN_ACTIVE 1
|
#define GPS_EN_ACTIVE 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
|
||||||
|
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GNSS_MODEL_ATGM336H,
|
GNSS_MODEL_ATGM336H,
|
||||||
GNSS_MODEL_MTK,
|
GNSS_MODEL_MTK,
|
||||||
@@ -31,7 +34,8 @@ typedef enum {
|
|||||||
GNSS_MODEL_MTK_PA1616S,
|
GNSS_MODEL_MTK_PA1616S,
|
||||||
GNSS_MODEL_AG3335,
|
GNSS_MODEL_AG3335,
|
||||||
GNSS_MODEL_AG3352,
|
GNSS_MODEL_AG3352,
|
||||||
GNSS_MODEL_LS20031
|
GNSS_MODEL_LS20031,
|
||||||
|
GNSS_MODEL_CM121
|
||||||
} GnssModel_t;
|
} GnssModel_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -150,6 +154,8 @@ class GPS : private concurrency::OSThread
|
|||||||
TinyGPSPlus reader;
|
TinyGPSPlus reader;
|
||||||
uint8_t fixQual = 0; // fix quality from GPGGA
|
uint8_t fixQual = 0; // fix quality from GPGGA
|
||||||
uint32_t lastChecksumFailCount = 0;
|
uint32_t lastChecksumFailCount = 0;
|
||||||
|
uint8_t currentStep = 0;
|
||||||
|
int32_t currentDelay = 2000;
|
||||||
|
|
||||||
#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS
|
#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||||
// (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field
|
// (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field
|
||||||
@@ -172,8 +178,6 @@ class GPS : private concurrency::OSThread
|
|||||||
*/
|
*/
|
||||||
bool hasValidLocation = false; // default to false, until we complete our first read
|
bool hasValidLocation = false; // default to false, until we complete our first read
|
||||||
|
|
||||||
bool isInPowersave = false;
|
|
||||||
|
|
||||||
bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop()
|
bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop()
|
||||||
|
|
||||||
bool hasGPS = false; // Do we have a GPS we are talking to
|
bool hasGPS = false; // Do we have a GPS we are talking to
|
||||||
|
|||||||
@@ -109,6 +109,35 @@ RTCSetResult readFromRTC()
|
|||||||
}
|
}
|
||||||
return RTCSetResultSuccess;
|
return RTCSetResultSuccess;
|
||||||
}
|
}
|
||||||
|
#elif defined(RX8130CE_RTC)
|
||||||
|
if (rtc_found.address == RX8130CE_RTC) {
|
||||||
|
uint32_t now = millis();
|
||||||
|
ArtronShop_RX8130CE rtc(&Wire);
|
||||||
|
tm t;
|
||||||
|
if (rtc.getTime(&t)) {
|
||||||
|
tv.tv_sec = gm_mktime(&t);
|
||||||
|
tv.tv_usec = 0;
|
||||||
|
|
||||||
|
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||||
|
LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
|
||||||
|
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
|
||||||
|
#ifdef BUILD_EPOCH
|
||||||
|
if (tv.tv_sec < BUILD_EPOCH) {
|
||||||
|
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||||
|
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||||
|
lastTimeValidationWarning = millis();
|
||||||
|
}
|
||||||
|
return RTCSetResultInvalidTime;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (currentQuality == RTCQualityNone) {
|
||||||
|
timeStartMsec = now;
|
||||||
|
zeroOffsetSecs = tv.tv_sec;
|
||||||
|
currentQuality = RTCQualityDevice;
|
||||||
|
}
|
||||||
|
return RTCSetResultSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
if (!gettimeofday(&tv, NULL)) {
|
if (!gettimeofday(&tv, NULL)) {
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
@@ -214,6 +243,17 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
|||||||
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
|
||||||
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||||
}
|
}
|
||||||
|
#elif defined(RX8130CE_RTC)
|
||||||
|
if (rtc_found.address == RX8130CE_RTC) {
|
||||||
|
ArtronShop_RX8130CE rtc(&Wire);
|
||||||
|
tm *t = gmtime(&tv->tv_sec);
|
||||||
|
if (rtc.setTime(*t)) {
|
||||||
|
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
|
||||||
|
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Failed to set time for RX8130CE");
|
||||||
|
}
|
||||||
|
}
|
||||||
#elif defined(ARCH_ESP32)
|
#elif defined(ARCH_ESP32)
|
||||||
settimeofday(tv, NULL);
|
settimeofday(tv, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
#include "sys/time.h"
|
#include "sys/time.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifdef RX8130CE_RTC
|
||||||
|
#include <ArtronShop_RX8130CE.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
enum RTCQuality {
|
enum RTCQuality {
|
||||||
|
|
||||||
/// We haven't had our RTC set yet
|
/// We haven't had our RTC set yet
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ bool EInkDisplay::connect()
|
|||||||
adafruitDisplay->setRotation(1);
|
adafruitDisplay->setRotation(1);
|
||||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||||
}
|
}
|
||||||
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||||
{
|
{
|
||||||
spi1 = &SPI1;
|
spi1 = &SPI1;
|
||||||
spi1->begin();
|
spi1->begin();
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay
|
|||||||
SPIClass *hspi = NULL;
|
SPIClass *hspi = NULL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
|
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||||
SPIClass *spi1 = NULL;
|
SPIClass *spi1 = NULL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
687
src/graphics/Panel_sdl.cpp
Normal file
687
src/graphics/Panel_sdl.cpp
Normal file
@@ -0,0 +1,687 @@
|
|||||||
|
/*----------------------------------------------------------------------------/
|
||||||
|
Lovyan GFX - Graphics library for embedded devices.
|
||||||
|
|
||||||
|
Original Source:
|
||||||
|
https://github.com/lovyan03/LovyanGFX/
|
||||||
|
|
||||||
|
Licence:
|
||||||
|
[FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt)
|
||||||
|
|
||||||
|
Author:
|
||||||
|
[lovyan03](https://twitter.com/lovyan03)
|
||||||
|
|
||||||
|
Contributors:
|
||||||
|
[ciniml](https://github.com/ciniml)
|
||||||
|
[mongonta0716](https://github.com/mongonta0716)
|
||||||
|
[tobozo](https://github.com/tobozo)
|
||||||
|
|
||||||
|
Porting for SDL:
|
||||||
|
[imliubo](https://github.com/imliubo)
|
||||||
|
/----------------------------------------------------------------------------*/
|
||||||
|
#include "Panel_sdl.hpp"
|
||||||
|
|
||||||
|
#if defined(SDL_h_)
|
||||||
|
|
||||||
|
// #include "../common.hpp"
|
||||||
|
// #include "../../misc/common_function.hpp"
|
||||||
|
// #include "../../Bus.hpp"
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
#include <math.h>
|
||||||
|
#include <vector>
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace lgfx
|
||||||
|
{
|
||||||
|
inline namespace v1
|
||||||
|
{
|
||||||
|
SDL_Keymod Panel_sdl::_keymod = KMOD_NONE;
|
||||||
|
static SDL_semaphore *_update_in_semaphore = nullptr;
|
||||||
|
static SDL_semaphore *_update_out_semaphore = nullptr;
|
||||||
|
volatile static uint32_t _in_step_exec = 0;
|
||||||
|
volatile static uint32_t _msec_step_exec = 512;
|
||||||
|
static bool _inited = false;
|
||||||
|
static bool _all_close = false;
|
||||||
|
|
||||||
|
volatile uint8_t Panel_sdl::_gpio_dummy_values[EMULATED_GPIO_MAX];
|
||||||
|
|
||||||
|
static inline void *heap_alloc_dma(size_t length)
|
||||||
|
{
|
||||||
|
return malloc(length);
|
||||||
|
} // aligned_alloc(16, length);
|
||||||
|
static inline void heap_free(void *buf)
|
||||||
|
{
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::list<monitor_t *> _list_monitor;
|
||||||
|
|
||||||
|
static monitor_t *const getMonitorByWindowID(uint32_t windowID)
|
||||||
|
{
|
||||||
|
for (auto &m : _list_monitor) {
|
||||||
|
if (SDL_GetWindowID(m->window) == windowID) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static std::vector<Panel_sdl::KeyCodeMapping_t> _key_code_map;
|
||||||
|
|
||||||
|
void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio)
|
||||||
|
{
|
||||||
|
if (gpio > EMULATED_GPIO_MAX)
|
||||||
|
return;
|
||||||
|
KeyCodeMapping_t map;
|
||||||
|
map.keycode = keyCode;
|
||||||
|
map.gpio = gpio;
|
||||||
|
_key_code_map.push_back(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode)
|
||||||
|
{
|
||||||
|
for (const auto &i : _key_code_map) {
|
||||||
|
if (i.keycode == keyCode)
|
||||||
|
return i.gpio;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::_event_proc(void)
|
||||||
|
{
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) {
|
||||||
|
auto mon = getMonitorByWindowID(event.button.windowID);
|
||||||
|
int gpio = -1;
|
||||||
|
|
||||||
|
/// Check key mapping
|
||||||
|
gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym);
|
||||||
|
if (gpio < 0) {
|
||||||
|
switch (event.key.keysym.sym) { /// M5StackのBtnA~BtnCのエミュレート;
|
||||||
|
// case SDLK_LEFT: gpio = 39; break;
|
||||||
|
// case SDLK_DOWN: gpio = 38; break;
|
||||||
|
// case SDLK_RIGHT: gpio = 37; break;
|
||||||
|
// case SDLK_UP: gpio = 36; break;
|
||||||
|
|
||||||
|
/// L/Rキーで画面回転
|
||||||
|
case SDLK_r:
|
||||||
|
case SDLK_l:
|
||||||
|
if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) {
|
||||||
|
if (mon != nullptr) {
|
||||||
|
mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1);
|
||||||
|
int x, y, w, h;
|
||||||
|
SDL_GetWindowSize(mon->window, &w, &h);
|
||||||
|
SDL_GetWindowPosition(mon->window, &x, &y);
|
||||||
|
SDL_SetWindowSize(mon->window, h, w);
|
||||||
|
SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2);
|
||||||
|
mon->panel->sdl_invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
/// 1~6キーで画面拡大率変更
|
||||||
|
case SDLK_1:
|
||||||
|
case SDLK_2:
|
||||||
|
case SDLK_3:
|
||||||
|
case SDLK_4:
|
||||||
|
case SDLK_5:
|
||||||
|
case SDLK_6:
|
||||||
|
if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) {
|
||||||
|
if (mon != nullptr) {
|
||||||
|
int size = 1 + (event.key.keysym.sym - SDLK_1);
|
||||||
|
_update_scaling(mon, size, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type == SDL_KEYDOWN) {
|
||||||
|
Panel_sdl::gpio_lo(gpio);
|
||||||
|
} else {
|
||||||
|
Panel_sdl::gpio_hi(gpio);
|
||||||
|
}
|
||||||
|
} else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) {
|
||||||
|
auto mon = getMonitorByWindowID(event.button.windowID);
|
||||||
|
if (mon != nullptr) {
|
||||||
|
{
|
||||||
|
int x, y, w, h;
|
||||||
|
SDL_GetWindowSize(mon->window, &w, &h);
|
||||||
|
SDL_GetMouseState(&x, &y);
|
||||||
|
float sf = sinf(mon->frame_angle * M_PI / 180);
|
||||||
|
float cf = cosf(mon->frame_angle * M_PI / 180);
|
||||||
|
x -= w / 2.0f;
|
||||||
|
y -= h / 2.0f;
|
||||||
|
float nx = y * sf + x * cf;
|
||||||
|
float ny = y * cf - x * sf;
|
||||||
|
if (mon->frame_rotation & 1) {
|
||||||
|
std::swap(w, h);
|
||||||
|
}
|
||||||
|
x = (nx * mon->frame_width / w) + (mon->frame_width >> 1);
|
||||||
|
y = (ny * mon->frame_height / h) + (mon->frame_height >> 1);
|
||||||
|
mon->touch_x = x - mon->frame_inner_x;
|
||||||
|
mon->touch_y = y - mon->frame_inner_y;
|
||||||
|
}
|
||||||
|
if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
mon->touched = true;
|
||||||
|
}
|
||||||
|
if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) {
|
||||||
|
mon->touched = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.type == SDL_WINDOWEVENT) {
|
||||||
|
auto monitor = getMonitorByWindowID(event.window.windowID);
|
||||||
|
if (monitor) {
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
|
int mw, mh;
|
||||||
|
SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh);
|
||||||
|
if (monitor->frame_rotation & 1) {
|
||||||
|
std::swap(mw, mh);
|
||||||
|
}
|
||||||
|
monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f;
|
||||||
|
monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f;
|
||||||
|
monitor->panel->sdl_invalidate();
|
||||||
|
} else if (event.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||||
|
monitor->closing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.type == SDL_QUIT) {
|
||||||
|
for (auto &m : _list_monitor) {
|
||||||
|
m->closing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// デバッガでステップ実行されていることを検出するスレッド用関数。
|
||||||
|
static int detectDebugger(bool *running)
|
||||||
|
{
|
||||||
|
uint32_t prev_ms = SDL_GetTicks();
|
||||||
|
do {
|
||||||
|
SDL_Delay(1);
|
||||||
|
uint32_t ms = SDL_GetTicks();
|
||||||
|
/// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。
|
||||||
|
/// また、解除されたと判断した後も1023msecほど状態を維持する。
|
||||||
|
if (ms - prev_ms > 64) {
|
||||||
|
_in_step_exec = _msec_step_exec;
|
||||||
|
} else if (_in_step_exec) {
|
||||||
|
--_in_step_exec;
|
||||||
|
}
|
||||||
|
prev_ms = ms;
|
||||||
|
} while (*running);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::_update_proc(void)
|
||||||
|
{
|
||||||
|
for (auto it = _list_monitor.begin(); it != _list_monitor.end();) {
|
||||||
|
if ((*it)->closing) {
|
||||||
|
if ((*it)->texture_frameimage) {
|
||||||
|
SDL_DestroyTexture((*it)->texture_frameimage);
|
||||||
|
}
|
||||||
|
SDL_DestroyTexture((*it)->texture);
|
||||||
|
SDL_DestroyRenderer((*it)->renderer);
|
||||||
|
SDL_DestroyWindow((*it)->window);
|
||||||
|
_list_monitor.erase(it++);
|
||||||
|
if (_list_monitor.empty()) {
|
||||||
|
_all_close = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
(*it)->panel->sdl_update();
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Panel_sdl::setup(void)
|
||||||
|
{
|
||||||
|
if (_inited)
|
||||||
|
return 1;
|
||||||
|
_inited = true;
|
||||||
|
|
||||||
|
/// Add default keycode mapping
|
||||||
|
/// M5StackのBtnA~BtnCのエミュレート;
|
||||||
|
addKeyCodeMapping(SDLK_LEFT, 39);
|
||||||
|
addKeyCodeMapping(SDLK_DOWN, 38);
|
||||||
|
addKeyCodeMapping(SDLK_RIGHT, 37);
|
||||||
|
addKeyCodeMapping(SDLK_UP, 36);
|
||||||
|
|
||||||
|
SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited);
|
||||||
|
|
||||||
|
_update_in_semaphore = SDL_CreateSemaphore(0);
|
||||||
|
_update_out_semaphore = SDL_CreateSemaphore(0);
|
||||||
|
for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) {
|
||||||
|
gpio_hi(pin);
|
||||||
|
}
|
||||||
|
/*Initialize the SDL*/
|
||||||
|
SDL_Init(SDL_INIT_VIDEO);
|
||||||
|
SDL_StartTextInput();
|
||||||
|
|
||||||
|
// SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Panel_sdl::loop(void)
|
||||||
|
{
|
||||||
|
if (!_inited)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
_event_proc();
|
||||||
|
SDL_SemWaitTimeout(_update_in_semaphore, 1);
|
||||||
|
_update_proc();
|
||||||
|
_event_proc();
|
||||||
|
if (SDL_SemValue(_update_out_semaphore) == 0) {
|
||||||
|
SDL_SemPost(_update_out_semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _all_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Panel_sdl::close(void)
|
||||||
|
{
|
||||||
|
if (!_inited)
|
||||||
|
return 1;
|
||||||
|
_inited = false;
|
||||||
|
|
||||||
|
SDL_StopTextInput();
|
||||||
|
SDL_DestroySemaphore(_update_in_semaphore);
|
||||||
|
SDL_DestroySemaphore(_update_out_semaphore);
|
||||||
|
SDL_Quit();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec)
|
||||||
|
{
|
||||||
|
_msec_step_exec = msec_step_exec;
|
||||||
|
|
||||||
|
/// SDLの準備
|
||||||
|
if (0 != Panel_sdl::setup()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ユーザコード関数の動作・停止フラグ
|
||||||
|
bool running = true;
|
||||||
|
|
||||||
|
/// ユーザコード関数を起動する
|
||||||
|
auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running);
|
||||||
|
|
||||||
|
/// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続
|
||||||
|
while (0 == Panel_sdl::loop()) {
|
||||||
|
};
|
||||||
|
|
||||||
|
/// ユーザコード関数を終了する
|
||||||
|
running = false;
|
||||||
|
SDL_WaitThread(thread, nullptr);
|
||||||
|
|
||||||
|
/// SDLを終了する
|
||||||
|
return Panel_sdl::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y)
|
||||||
|
{
|
||||||
|
monitor.scaling_x = scaling_x;
|
||||||
|
monitor.scaling_y = scaling_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y)
|
||||||
|
{
|
||||||
|
monitor.frame_image = frame_image;
|
||||||
|
monitor.frame_width = frame_width;
|
||||||
|
monitor.frame_height = frame_height;
|
||||||
|
monitor.frame_inner_x = inner_x;
|
||||||
|
monitor.frame_inner_y = inner_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation)
|
||||||
|
{
|
||||||
|
monitor.frame_rotation = frame_rotation;
|
||||||
|
monitor.frame_angle = (monitor.frame_rotation) * 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel_sdl::~Panel_sdl(void)
|
||||||
|
{
|
||||||
|
_list_monitor.remove(&monitor);
|
||||||
|
SDL_DestroyMutex(_sdl_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase()
|
||||||
|
{
|
||||||
|
_sdl_mutex = SDL_CreateMutex();
|
||||||
|
_auto_display = true;
|
||||||
|
monitor.panel = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Panel_sdl::init(bool use_reset)
|
||||||
|
{
|
||||||
|
initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height);
|
||||||
|
bool res = Panel_FrameBufferBase::init(use_reset);
|
||||||
|
|
||||||
|
_list_monitor.push_back(&monitor);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
color_depth_t Panel_sdl::setColorDepth(color_depth_t depth)
|
||||||
|
{
|
||||||
|
auto bits = depth & color_depth_t::bit_mask;
|
||||||
|
if (bits >= 16) {
|
||||||
|
depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte;
|
||||||
|
} else {
|
||||||
|
depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte;
|
||||||
|
}
|
||||||
|
_write_depth = depth;
|
||||||
|
_read_depth = depth;
|
||||||
|
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent}
|
||||||
|
{
|
||||||
|
SDL_LockMutex(parent->_sdl_mutex);
|
||||||
|
};
|
||||||
|
|
||||||
|
Panel_sdl::lock_t::~lock_t(void)
|
||||||
|
{
|
||||||
|
++_parent->_modified_counter;
|
||||||
|
SDL_UnlockMutex(_parent->_sdl_mutex);
|
||||||
|
if (SDL_SemValue(_update_in_semaphore) < 2) {
|
||||||
|
SDL_SemPost(_update_in_semaphore);
|
||||||
|
if (!_in_step_exec) {
|
||||||
|
SDL_SemWaitTimeout(_update_out_semaphore, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor)
|
||||||
|
{
|
||||||
|
lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor)
|
||||||
|
{
|
||||||
|
lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length)
|
||||||
|
{
|
||||||
|
// lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::writeBlock(rawcolor, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma)
|
||||||
|
{
|
||||||
|
lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param)
|
||||||
|
{
|
||||||
|
lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma)
|
||||||
|
{
|
||||||
|
lock_t lock(this);
|
||||||
|
Panel_FrameBufferBase::writePixels(param, len, use_dma);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h)
|
||||||
|
{
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)w;
|
||||||
|
(void)h;
|
||||||
|
if (_in_step_exec) {
|
||||||
|
if (_display_counter != _modified_counter) {
|
||||||
|
do {
|
||||||
|
SDL_SemPost(_update_in_semaphore);
|
||||||
|
SDL_SemWaitTimeout(_update_out_semaphore, 1);
|
||||||
|
} while (_display_counter != _modified_counter);
|
||||||
|
SDL_Delay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count)
|
||||||
|
{
|
||||||
|
(void)count;
|
||||||
|
tp->x = monitor.touch_x;
|
||||||
|
tp->y = monitor.touch_y;
|
||||||
|
tp->size = monitor.touched ? 1 : 0;
|
||||||
|
tp->id = 0;
|
||||||
|
return monitor.touched;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::setWindowTitle(const char *title)
|
||||||
|
{
|
||||||
|
_window_title = title;
|
||||||
|
if (monitor.window) {
|
||||||
|
SDL_SetWindowTitle(monitor.window, _window_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy)
|
||||||
|
{
|
||||||
|
mon->scaling_x = sx;
|
||||||
|
mon->scaling_y = sy;
|
||||||
|
int nw = mon->frame_width;
|
||||||
|
int nh = mon->frame_height;
|
||||||
|
if (mon->frame_rotation & 1) {
|
||||||
|
std::swap(nw, nh);
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y, w, h;
|
||||||
|
int rw, rh;
|
||||||
|
SDL_GetRendererOutputSize(mon->renderer, &rw, &rh);
|
||||||
|
SDL_GetWindowSize(mon->window, &w, &h);
|
||||||
|
nw = nw * sx * w / rw;
|
||||||
|
nh = nh * sy * h / rh;
|
||||||
|
SDL_GetWindowPosition(mon->window, &x, &y);
|
||||||
|
SDL_SetWindowSize(mon->window, nw, nh);
|
||||||
|
SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2);
|
||||||
|
mon->panel->sdl_invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::sdl_create(monitor_t *m)
|
||||||
|
{
|
||||||
|
int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
|
#if SDL_FULLSCREEN
|
||||||
|
flag |= SDL_WINDOW_FULLSCREEN;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m->frame_width < _cfg.panel_width) {
|
||||||
|
m->frame_width = _cfg.panel_width;
|
||||||
|
}
|
||||||
|
if (m->frame_height < _cfg.panel_height) {
|
||||||
|
m->frame_height = _cfg.panel_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
int window_width = m->frame_width * m->scaling_x;
|
||||||
|
int window_height = m->frame_height * m->scaling_y;
|
||||||
|
int scaling_x = m->scaling_x;
|
||||||
|
int scaling_y = m->scaling_y;
|
||||||
|
if (m->frame_rotation & 1) {
|
||||||
|
std::swap(window_width, window_height);
|
||||||
|
std::swap(scaling_x, scaling_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height,
|
||||||
|
flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/
|
||||||
|
}
|
||||||
|
m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
||||||
|
m->texture =
|
||||||
|
SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height);
|
||||||
|
SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE);
|
||||||
|
|
||||||
|
if (m->frame_image) {
|
||||||
|
// 枠画像用のサーフェイスを作成
|
||||||
|
auto sf = SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4,
|
||||||
|
0xFF000000, 0xFF0000, 0xFF00, 0xFF);
|
||||||
|
if (sf != nullptr) {
|
||||||
|
// 枠画像からテクスチャを作成
|
||||||
|
m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf);
|
||||||
|
SDL_FreeSurface(sf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND);
|
||||||
|
_update_scaling(m, scaling_x, scaling_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::sdl_update(void)
|
||||||
|
{
|
||||||
|
if (monitor.renderer == nullptr) {
|
||||||
|
sdl_create(&monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool step_exec = _in_step_exec;
|
||||||
|
|
||||||
|
if (_texupdate_counter != _modified_counter) {
|
||||||
|
pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false);
|
||||||
|
if (_write_depth == rgb565_2Byte) {
|
||||||
|
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, swap565_t>;
|
||||||
|
} else if (_write_depth == rgb888_3Byte) {
|
||||||
|
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, bgr888_t>;
|
||||||
|
} else if (_write_depth == rgb332_1Byte) {
|
||||||
|
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, rgb332_t>;
|
||||||
|
} else if (_write_depth == grayscale_8bit) {
|
||||||
|
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, grayscale_t>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 == SDL_LockMutex(_sdl_mutex)) {
|
||||||
|
_texupdate_counter = _modified_counter;
|
||||||
|
for (int y = 0; y < _cfg.panel_height; ++y) {
|
||||||
|
pc.src_x32 = 0;
|
||||||
|
pc.src_data = _lines_buffer[y];
|
||||||
|
pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc);
|
||||||
|
}
|
||||||
|
SDL_UnlockMutex(_sdl_mutex);
|
||||||
|
SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int angle = monitor.frame_angle;
|
||||||
|
int target = (monitor.frame_rotation) * 90;
|
||||||
|
angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3);
|
||||||
|
|
||||||
|
if (monitor.frame_angle != angle) { // 表示する向きを変える
|
||||||
|
monitor.frame_angle = angle;
|
||||||
|
sdl_invalidate();
|
||||||
|
} else if (monitor.frame_rotation & ~3u) {
|
||||||
|
monitor.frame_rotation &= 3;
|
||||||
|
monitor.frame_angle = (monitor.frame_rotation) * 90;
|
||||||
|
sdl_invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_invalidated || (_display_counter != _texupdate_counter)) {
|
||||||
|
SDL_RendererInfo info;
|
||||||
|
if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) {
|
||||||
|
// ステップ実行中はVSYNCを待機しない
|
||||||
|
if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) {
|
||||||
|
SDL_RenderSetVSync(monitor.renderer, !step_exec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
int red = 0;
|
||||||
|
int green = 0;
|
||||||
|
int blue = 0;
|
||||||
|
#if defined(M5GFX_BACK_COLOR)
|
||||||
|
red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF;
|
||||||
|
green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF;
|
||||||
|
blue = ((M5GFX_BACK_COLOR)) & 0xFF;
|
||||||
|
#endif
|
||||||
|
SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF);
|
||||||
|
}
|
||||||
|
SDL_RenderClear(monitor.renderer);
|
||||||
|
if (_invalidated) {
|
||||||
|
_invalidated = false;
|
||||||
|
int mw, mh;
|
||||||
|
SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh);
|
||||||
|
}
|
||||||
|
render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle);
|
||||||
|
render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle);
|
||||||
|
SDL_RenderPresent(monitor.renderer);
|
||||||
|
_display_counter = _texupdate_counter;
|
||||||
|
if (_invalidated) {
|
||||||
|
_invalidated = false;
|
||||||
|
SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF);
|
||||||
|
SDL_RenderClear(monitor.renderer);
|
||||||
|
render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height,
|
||||||
|
angle);
|
||||||
|
render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle);
|
||||||
|
SDL_RenderPresent(monitor.renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle)
|
||||||
|
{
|
||||||
|
SDL_Point pivot;
|
||||||
|
pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x;
|
||||||
|
pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y;
|
||||||
|
SDL_Rect dstrect;
|
||||||
|
dstrect.w = tw * monitor.scaling_x;
|
||||||
|
dstrect.h = th * monitor.scaling_y;
|
||||||
|
int mw, mh;
|
||||||
|
SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh);
|
||||||
|
dstrect.x = mw / 2.0f - pivot.x;
|
||||||
|
dstrect.y = mh / 2.0f - pivot.y;
|
||||||
|
SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Panel_sdl::initFrameBuffer(size_t width, size_t height)
|
||||||
|
{
|
||||||
|
uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *));
|
||||||
|
if (nullptr == lineArray) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t));
|
||||||
|
|
||||||
|
/// 8byte alignment;
|
||||||
|
width = (width + 7) & ~7u;
|
||||||
|
|
||||||
|
_lines_buffer = lineArray;
|
||||||
|
memset(lineArray, 0, height * sizeof(uint8_t *));
|
||||||
|
|
||||||
|
uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16);
|
||||||
|
|
||||||
|
auto fb = framebuffer;
|
||||||
|
{
|
||||||
|
for (size_t y = 0; y < height; ++y) {
|
||||||
|
lineArray[y] = fb;
|
||||||
|
fb += width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Panel_sdl::deinitFrameBuffer(void)
|
||||||
|
{
|
||||||
|
auto lines = _lines_buffer;
|
||||||
|
_lines_buffer = nullptr;
|
||||||
|
if (lines != nullptr) {
|
||||||
|
heap_free(lines[0]);
|
||||||
|
heap_free(lines);
|
||||||
|
}
|
||||||
|
if (_texturebuf) {
|
||||||
|
heap_free(_texturebuf);
|
||||||
|
_texturebuf = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
} // namespace v1
|
||||||
|
} // namespace lgfx
|
||||||
|
|
||||||
|
#endif
|
||||||
166
src/graphics/Panel_sdl.hpp
Normal file
166
src/graphics/Panel_sdl.hpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/*----------------------------------------------------------------------------/
|
||||||
|
Lovyan GFX - Graphics library for embedded devices.
|
||||||
|
|
||||||
|
Original Source:
|
||||||
|
https://github.com/lovyan03/LovyanGFX/
|
||||||
|
|
||||||
|
Licence:
|
||||||
|
[FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt)
|
||||||
|
|
||||||
|
Author:
|
||||||
|
[lovyan03](https://twitter.com/lovyan03)
|
||||||
|
|
||||||
|
Contributors:
|
||||||
|
[ciniml](https://github.com/ciniml)
|
||||||
|
[mongonta0716](https://github.com/mongonta0716)
|
||||||
|
[tobozo](https://github.com/tobozo)
|
||||||
|
|
||||||
|
Porting for SDL:
|
||||||
|
[imliubo](https://github.com/imliubo)
|
||||||
|
/----------------------------------------------------------------------------*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define SDL_MAIN_HANDLED
|
||||||
|
// cppcheck-suppress preprocessorErrorDirective
|
||||||
|
#if __has_include(<SDL2/SDL.h>)
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_main.h>
|
||||||
|
#elif __has_include(<SDL.h>)
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_main.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(SDL_h_)
|
||||||
|
#include "lgfx/v1/Touch.hpp"
|
||||||
|
#include "lgfx/v1/misc/range.hpp"
|
||||||
|
#include "lgfx/v1/panel/Panel_FrameBufferBase.hpp"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace lgfx
|
||||||
|
{
|
||||||
|
inline namespace v1
|
||||||
|
{
|
||||||
|
|
||||||
|
struct Panel_sdl;
|
||||||
|
struct monitor_t {
|
||||||
|
SDL_Window *window = nullptr;
|
||||||
|
SDL_Renderer *renderer = nullptr;
|
||||||
|
SDL_Texture *texture = nullptr;
|
||||||
|
SDL_Texture *texture_frameimage = nullptr;
|
||||||
|
Panel_sdl *panel = nullptr;
|
||||||
|
|
||||||
|
// 外枠
|
||||||
|
const void *frame_image = 0;
|
||||||
|
uint_fast16_t frame_width = 0;
|
||||||
|
uint_fast16_t frame_height = 0;
|
||||||
|
uint_fast16_t frame_inner_x = 0;
|
||||||
|
uint_fast16_t frame_inner_y = 0;
|
||||||
|
int_fast16_t frame_rotation = 0;
|
||||||
|
int_fast16_t frame_angle = 0;
|
||||||
|
|
||||||
|
float scaling_x = 1;
|
||||||
|
float scaling_y = 1;
|
||||||
|
int_fast16_t touch_x, touch_y;
|
||||||
|
bool touched = false;
|
||||||
|
bool closing = false;
|
||||||
|
};
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Touch_sdl : public ITouch {
|
||||||
|
bool init(void) override { return true; }
|
||||||
|
void wakeup(void) override {}
|
||||||
|
void sleep(void) override {}
|
||||||
|
bool isEnable(void) override { return true; };
|
||||||
|
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Panel_sdl : public Panel_FrameBufferBase {
|
||||||
|
static constexpr size_t EMULATED_GPIO_MAX = 128;
|
||||||
|
static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX];
|
||||||
|
|
||||||
|
public:
|
||||||
|
Panel_sdl(void);
|
||||||
|
virtual ~Panel_sdl(void);
|
||||||
|
|
||||||
|
bool init(bool use_reset) override;
|
||||||
|
|
||||||
|
color_depth_t setColorDepth(color_depth_t depth) override;
|
||||||
|
|
||||||
|
void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override;
|
||||||
|
|
||||||
|
// void setInvert(bool invert) override {}
|
||||||
|
void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override;
|
||||||
|
void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override;
|
||||||
|
void writeBlock(uint32_t rawcolor, uint32_t length) override;
|
||||||
|
void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param,
|
||||||
|
bool use_dma) override;
|
||||||
|
void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override;
|
||||||
|
void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override;
|
||||||
|
|
||||||
|
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override;
|
||||||
|
|
||||||
|
void setWindowTitle(const char *title);
|
||||||
|
void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y);
|
||||||
|
void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y);
|
||||||
|
void setFrameRotation(uint_fast16_t frame_rotaion);
|
||||||
|
void setBrightness(uint8_t brightness) override{};
|
||||||
|
|
||||||
|
static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; }
|
||||||
|
static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; }
|
||||||
|
static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; }
|
||||||
|
|
||||||
|
static int setup(void);
|
||||||
|
static int loop(void);
|
||||||
|
static int close(void);
|
||||||
|
|
||||||
|
static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512);
|
||||||
|
|
||||||
|
static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; }
|
||||||
|
|
||||||
|
struct KeyCodeMapping_t {
|
||||||
|
SDL_KeyCode keycode = SDLK_UNKNOWN;
|
||||||
|
uint8_t gpio = 0;
|
||||||
|
};
|
||||||
|
static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio);
|
||||||
|
static int getKeyCodeMapping(SDL_KeyCode keyCode);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const char *_window_title = "LGFX Simulator";
|
||||||
|
SDL_mutex *_sdl_mutex = nullptr;
|
||||||
|
|
||||||
|
void sdl_create(monitor_t *m);
|
||||||
|
void sdl_update(void);
|
||||||
|
|
||||||
|
touch_point_t _touch_point;
|
||||||
|
monitor_t monitor;
|
||||||
|
|
||||||
|
rgb888_t *_texturebuf = nullptr;
|
||||||
|
uint_fast16_t _modified_counter;
|
||||||
|
uint_fast16_t _texupdate_counter;
|
||||||
|
uint_fast16_t _display_counter;
|
||||||
|
bool _invalidated;
|
||||||
|
|
||||||
|
static void _event_proc(void);
|
||||||
|
static void _update_proc(void);
|
||||||
|
static void _update_scaling(monitor_t *m, float sx, float sy);
|
||||||
|
void sdl_invalidate(void) { _invalidated = true; }
|
||||||
|
void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle);
|
||||||
|
bool initFrameBuffer(size_t width, size_t height);
|
||||||
|
void deinitFrameBuffer(void);
|
||||||
|
|
||||||
|
static SDL_Keymod _keymod;
|
||||||
|
|
||||||
|
struct lock_t {
|
||||||
|
lock_t(Panel_sdl *parent);
|
||||||
|
~lock_t();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Panel_sdl *_parent;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
} // namespace v1
|
||||||
|
} // namespace lgfx
|
||||||
|
#endif
|
||||||
@@ -25,6 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "PowerMon.h"
|
#include "PowerMon.h"
|
||||||
#include "Throttle.h"
|
#include "Throttle.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "meshUtils.h"
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
|
||||||
@@ -58,7 +59,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/Channels.h"
|
#include "mesh/Channels.h"
|
||||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
#include "meshUtils.h"
|
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
@@ -83,6 +83,11 @@ extern uint16_t TFT_MESH;
|
|||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(T_LORA_PAGER)
|
||||||
|
// KB backlight control
|
||||||
|
#include "input/cardKbI2cImpl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace meshtastic; /** @todo remove */
|
using namespace meshtastic; /** @todo remove */
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -95,7 +100,7 @@ namespace graphics
|
|||||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||||
// if defined a pixel will blink to show redraws
|
// if defined a pixel will blink to show redraws
|
||||||
// #define SHOW_REDRAWS
|
// #define SHOW_REDRAWS
|
||||||
|
#define ASCII_BELL '\x07'
|
||||||
// A text message frame + debug frame + all the node infos
|
// A text message frame + debug frame + all the node infos
|
||||||
FrameCallback *normalFrames;
|
FrameCallback *normalFrames;
|
||||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||||
@@ -216,6 +221,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
ui->update();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
uint8_t module_frame;
|
uint8_t module_frame;
|
||||||
@@ -340,7 +383,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
|||||||
(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);
|
||||||
#elif ARCH_PORTDUINO
|
#elif ARCH_PORTDUINO
|
||||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
if (settingsMap[displayPanel] != no_screen) {
|
if (portduino_config.displayPanel != no_screen) {
|
||||||
LOG_DEBUG("Make TFTDisplay!");
|
LOG_DEBUG("Make TFTDisplay!");
|
||||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
dispdev = new TFTDisplay(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);
|
||||||
@@ -410,7 +453,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
dispdev->displayOn();
|
dispdev->displayOn();
|
||||||
#ifdef HELTEC_TRACKER_V1_X
|
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
|
||||||
ui->init();
|
ui->init();
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ST7789
|
#ifdef USE_ST7789
|
||||||
@@ -588,7 +631,7 @@ void Screen::setup()
|
|||||||
|
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||||
if (settingsMap[touchscreenModule]) {
|
if (portduino_config.touchscreenModule) {
|
||||||
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();
|
||||||
@@ -617,6 +660,19 @@ void Screen::setup()
|
|||||||
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Screen::setOn(bool on, FrameCallback einkScreensaver)
|
||||||
|
{
|
||||||
|
#if defined(T_LORA_PAGER)
|
||||||
|
if (cardKbI2cImpl)
|
||||||
|
cardKbI2cImpl->toggleBacklight(on);
|
||||||
|
#endif
|
||||||
|
if (!on)
|
||||||
|
// We handle off commands immediately, because they might be called because the CPU is shutting down
|
||||||
|
handleSetOn(false, einkScreensaver);
|
||||||
|
else
|
||||||
|
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::forceDisplay(bool forceUiUpdate)
|
void Screen::forceDisplay(bool forceUiUpdate)
|
||||||
{
|
{
|
||||||
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
|
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
|
||||||
@@ -725,13 +781,19 @@ int32_t Screen::runOnce()
|
|||||||
handleSetOn(false);
|
handleSetOn(false);
|
||||||
break;
|
break;
|
||||||
case Cmd::ON_PRESS:
|
case Cmd::ON_PRESS:
|
||||||
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
handleOnPress();
|
handleOnPress();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_PREV_FRAME:
|
case Cmd::SHOW_PREV_FRAME:
|
||||||
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
handleShowPrevFrame();
|
handleShowPrevFrame();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::SHOW_NEXT_FRAME:
|
case Cmd::SHOW_NEXT_FRAME:
|
||||||
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
handleShowNextFrame();
|
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
|
||||||
@@ -753,7 +815,9 @@ 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
|
||||||
|
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||||
setFrames();
|
setFrames();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Cmd::NOOP:
|
case Cmd::NOOP:
|
||||||
break;
|
break;
|
||||||
@@ -789,6 +853,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
|
||||||
@@ -879,6 +944,11 @@ 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
|
||||||
@@ -901,6 +971,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if defined(DISPLAY_CLOCK_FRAME)
|
#if defined(DISPLAY_CLOCK_FRAME)
|
||||||
|
if (!hiddenFrames.clock) {
|
||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
|
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
|
||||||
@@ -909,67 +980,86 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||||
#endif
|
#endif
|
||||||
indicatorIcons.push_back(digital_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
|
||||||
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||||||
|
|
||||||
|
if (!hiddenFrames.home) {
|
||||||
fsi.positions.home = numframes;
|
fsi.positions.home = numframes;
|
||||||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||||||
indicatorIcons.push_back(icon_home);
|
indicatorIcons.push_back(icon_home);
|
||||||
|
}
|
||||||
|
|
||||||
fsi.positions.textMessage = numframes;
|
fsi.positions.textMessage = numframes;
|
||||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||||
indicatorIcons.push_back(icon_mail);
|
indicatorIcons.push_back(icon_mail);
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
|
if (!hiddenFrames.nodelist) {
|
||||||
fsi.positions.nodelist = numframes;
|
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
|
||||||
|
if (!hiddenFrames.nodelist_lastheard) {
|
||||||
fsi.positions.nodelist_lastheard = numframes;
|
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);
|
||||||
|
}
|
||||||
|
if (!hiddenFrames.nodelist_hopsignal) {
|
||||||
fsi.positions.nodelist_hopsignal = numframes;
|
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);
|
||||||
|
}
|
||||||
|
if (!hiddenFrames.nodelist_distance) {
|
||||||
fsi.positions.nodelist_distance = numframes;
|
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
|
||||||
|
if (!hiddenFrames.nodelist_bearings) {
|
||||||
fsi.positions.nodelist_bearings = numframes;
|
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);
|
||||||
|
}
|
||||||
|
if (!hiddenFrames.gps) {
|
||||||
fsi.positions.gps = numframes;
|
fsi.positions.gps = numframes;
|
||||||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||||||
indicatorIcons.push_back(icon_compass);
|
indicatorIcons.push_back(icon_compass);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (RadioLibInterface::instance) {
|
if (RadioLibInterface::instance && !hiddenFrames.lora) {
|
||||||
fsi.positions.lora = numframes;
|
fsi.positions.lora = numframes;
|
||||||
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
||||||
indicatorIcons.push_back(icon_radio);
|
indicatorIcons.push_back(icon_radio);
|
||||||
}
|
}
|
||||||
if (!dismissedFrames.memory) {
|
if (!hiddenFrames.system) {
|
||||||
fsi.positions.memory = numframes;
|
fsi.positions.system = numframes;
|
||||||
normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
|
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
|
||||||
indicatorIcons.push_back(icon_memory);
|
indicatorIcons.push_back(icon_system);
|
||||||
}
|
}
|
||||||
#if !defined(DISPLAY_CLOCK_FRAME)
|
#if !defined(DISPLAY_CLOCK_FRAME)
|
||||||
|
if (!hiddenFrames.clock) {
|
||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||||
indicatorIcons.push_back(digital_icon_clock);
|
indicatorIcons.push_back(digital_icon_clock);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if (!hiddenFrames.chirpy) {
|
||||||
|
fsi.positions.chirpy = numframes;
|
||||||
|
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
|
||||||
|
indicatorIcons.push_back(chirpy_small);
|
||||||
|
}
|
||||||
|
|
||||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||||
if (!dismissedFrames.wifi && isWifiAvailable()) {
|
if (!hiddenFrames.wifi && isWifiAvailable()) {
|
||||||
fsi.positions.wifi = numframes;
|
fsi.positions.wifi = numframes;
|
||||||
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
||||||
indicatorIcons.push_back(icon_wifi);
|
indicatorIcons.push_back(icon_wifi);
|
||||||
@@ -1011,6 +1101,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
if (numMeshNodes > 0)
|
if (numMeshNodes > 0)
|
||||||
numMeshNodes--;
|
numMeshNodes--;
|
||||||
|
|
||||||
|
if (!hiddenFrames.show_favorites) {
|
||||||
// Temporary array to hold favorite node frames
|
// Temporary array to hold favorite node frames
|
||||||
std::vector<FrameCallback> favoriteFrames;
|
std::vector<FrameCallback> favoriteFrames;
|
||||||
|
|
||||||
@@ -1033,6 +1124,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
fsi.positions.firstFavorite = 255;
|
fsi.positions.firstFavorite = 255;
|
||||||
fsi.positions.lastFavorite = 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
|
||||||
@@ -1070,7 +1162,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
ui->switchToFrame(fsi.positions.clock);
|
ui->switchToFrame(fsi.positions.clock);
|
||||||
break;
|
break;
|
||||||
case FOCUS_SYSTEM:
|
case FOCUS_SYSTEM:
|
||||||
ui->switchToFrame(fsi.positions.memory);
|
ui->switchToFrame(fsi.positions.system);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FOCUS_PRESERVE:
|
case FOCUS_PRESERVE:
|
||||||
@@ -1098,30 +1190,101 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
|
|||||||
setFastFramerate();
|
setFastFramerate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Screen::toggleFrameVisibility(const std::string &frameName)
|
||||||
|
{
|
||||||
|
#ifndef USE_EINK
|
||||||
|
if (frameName == "nodelist") {
|
||||||
|
hiddenFrames.nodelist = !hiddenFrames.nodelist;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_EINK
|
||||||
|
if (frameName == "nodelist_lastheard") {
|
||||||
|
hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard;
|
||||||
|
}
|
||||||
|
if (frameName == "nodelist_hopsignal") {
|
||||||
|
hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal;
|
||||||
|
}
|
||||||
|
if (frameName == "nodelist_distance") {
|
||||||
|
hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if HAS_GPS
|
||||||
|
if (frameName == "nodelist_bearings") {
|
||||||
|
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
|
||||||
|
}
|
||||||
|
if (frameName == "gps") {
|
||||||
|
hiddenFrames.gps = !hiddenFrames.gps;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (frameName == "lora") {
|
||||||
|
hiddenFrames.lora = !hiddenFrames.lora;
|
||||||
|
}
|
||||||
|
if (frameName == "clock") {
|
||||||
|
hiddenFrames.clock = !hiddenFrames.clock;
|
||||||
|
}
|
||||||
|
if (frameName == "show_favorites") {
|
||||||
|
hiddenFrames.show_favorites = !hiddenFrames.show_favorites;
|
||||||
|
}
|
||||||
|
if (frameName == "chirpy") {
|
||||||
|
hiddenFrames.chirpy = !hiddenFrames.chirpy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Screen::isFrameHidden(const std::string &frameName) const
|
||||||
|
{
|
||||||
|
#ifndef USE_EINK
|
||||||
|
if (frameName == "nodelist")
|
||||||
|
return hiddenFrames.nodelist;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_EINK
|
||||||
|
if (frameName == "nodelist_lastheard")
|
||||||
|
return hiddenFrames.nodelist_lastheard;
|
||||||
|
if (frameName == "nodelist_hopsignal")
|
||||||
|
return hiddenFrames.nodelist_hopsignal;
|
||||||
|
if (frameName == "nodelist_distance")
|
||||||
|
return hiddenFrames.nodelist_distance;
|
||||||
|
#endif
|
||||||
|
#if HAS_GPS
|
||||||
|
if (frameName == "nodelist_bearings")
|
||||||
|
return hiddenFrames.nodelist_bearings;
|
||||||
|
if (frameName == "gps")
|
||||||
|
return hiddenFrames.gps;
|
||||||
|
#endif
|
||||||
|
if (frameName == "lora")
|
||||||
|
return hiddenFrames.lora;
|
||||||
|
if (frameName == "clock")
|
||||||
|
return hiddenFrames.clock;
|
||||||
|
if (frameName == "show_favorites")
|
||||||
|
return hiddenFrames.show_favorites;
|
||||||
|
if (frameName == "chirpy")
|
||||||
|
return hiddenFrames.chirpy;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Dismisses the currently displayed screen frame, if possible
|
// Dismisses the currently displayed screen frame, if possible
|
||||||
// Relevant for text message, waypoint, others in future?
|
// Relevant for text message, waypoint, others in future?
|
||||||
// Triggered with a CardKB keycombo
|
// Triggered with a CardKB keycombo
|
||||||
void Screen::dismissCurrentFrame()
|
void Screen::hideCurrentFrame()
|
||||||
{
|
{
|
||||||
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||||
bool dismissed = false;
|
bool dismissed = false;
|
||||||
|
|
||||||
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||||||
LOG_INFO("Dismiss Text Message");
|
LOG_INFO("Hide Text Message");
|
||||||
devicestate.has_rx_text_message = false;
|
devicestate.has_rx_text_message = false;
|
||||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||||
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||||||
LOG_DEBUG("Dismiss Waypoint");
|
LOG_DEBUG("Hide Waypoint");
|
||||||
devicestate.has_rx_waypoint = false;
|
devicestate.has_rx_waypoint = false;
|
||||||
dismissedFrames.waypoint = true;
|
hiddenFrames.waypoint = true;
|
||||||
dismissed = true;
|
dismissed = true;
|
||||||
} else if (currentFrame == framesetInfo.positions.wifi) {
|
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||||||
LOG_DEBUG("Dismiss WiFi Screen");
|
LOG_DEBUG("Hide WiFi Screen");
|
||||||
dismissedFrames.wifi = true;
|
hiddenFrames.wifi = true;
|
||||||
dismissed = true;
|
dismissed = true;
|
||||||
} else if (currentFrame == framesetInfo.positions.memory) {
|
} else if (currentFrame == framesetInfo.positions.lora) {
|
||||||
LOG_INFO("Dismiss Memory");
|
LOG_INFO("Hide LoRa");
|
||||||
dismissedFrames.memory = true;
|
hiddenFrames.lora = true;
|
||||||
dismissed = true;
|
dismissed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,7 +1317,8 @@ void Screen::blink()
|
|||||||
delay(50);
|
delay(50);
|
||||||
count = count - 1;
|
count = count - 1;
|
||||||
}
|
}
|
||||||
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay.
|
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
|
||||||
|
// OLEDDisplay.
|
||||||
dispdev->setBrightness(brightness);
|
dispdev->setBrightness(brightness);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1264,6 +1428,9 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg)
|
|||||||
}
|
}
|
||||||
nodeDB->updateGUI = false;
|
nodeDB->updateGUI = false;
|
||||||
break;
|
break;
|
||||||
|
case STATUS_TYPE_POWER:
|
||||||
|
forceDisplay(true);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1277,7 +1444,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
// Outgoing message (likely sent from phone)
|
// Outgoing message (likely sent from phone)
|
||||||
devicestate.has_rx_text_message = false;
|
devicestate.has_rx_text_message = false;
|
||||||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||||||
dismissedFrames.textMessage = true;
|
hiddenFrames.textMessage = true;
|
||||||
hasUnreadMessage = false; // Clear unread state when user replies
|
hasUnreadMessage = false; // Clear unread state when user replies
|
||||||
|
|
||||||
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
|
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
|
||||||
@@ -1294,28 +1461,36 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
}
|
}
|
||||||
// === Prepare banner content ===
|
// === Prepare banner content ===
|
||||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||||
|
const meshtastic_Channel channel =
|
||||||
|
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||||
|
|
||||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||||
|
|
||||||
char banner[256];
|
char banner[256];
|
||||||
|
|
||||||
// Check for bell character in message to determine alert type
|
|
||||||
bool isAlert = false;
|
bool isAlert = false;
|
||||||
|
|
||||||
|
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra ||
|
||||||
|
moduleConfig.external_notification.alert_bell_buzzer)
|
||||||
|
// Check for bell character to determine if this message is an alert
|
||||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||||
if (msgRaw[i] == '\x07') {
|
if (msgRaw[i] == ASCII_BELL) {
|
||||||
isAlert = true;
|
isAlert = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
|
||||||
|
// 'mute' preferences set to any specific node or channel.
|
||||||
if (isAlert) {
|
if (isAlert) {
|
||||||
if (longName && longName[0]) {
|
if (longName && longName[0]) {
|
||||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||||
} else {
|
} else {
|
||||||
strcpy(banner, "Alert Received");
|
strcpy(banner, "Alert Received");
|
||||||
}
|
}
|
||||||
} else {
|
screen->showSimpleBanner(banner, 3000);
|
||||||
|
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
|
||||||
if (longName && longName[0]) {
|
if (longName && longName[0]) {
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
strcpy(banner, "New Message");
|
strcpy(banner, "New Message");
|
||||||
@@ -1326,16 +1501,23 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
} else {
|
} else {
|
||||||
strcpy(banner, "New Message");
|
strcpy(banner, "New Message");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
screen->setOn(true);
|
screen->setOn(true);
|
||||||
screen->showSimpleBanner(banner, 1500);
|
screen->showSimpleBanner(banner, 1500);
|
||||||
|
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
|
||||||
|
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
|
||||||
|
(!isBroadcast(packet->to) && isToUs(packet))) {
|
||||||
|
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
|
||||||
|
// - packet contains an alert and alert bell buzzer is enabled
|
||||||
|
// - packet is a non-broadcast that is addressed to this node
|
||||||
playLongBeep();
|
playLongBeep();
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
screen->showSimpleBanner(banner, 3000);
|
screen->showSimpleBanner(banner, 3000);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1343,6 +1525,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)
|
||||||
@@ -1365,6 +1552,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
|
||||||
@@ -1402,7 +1599,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
} 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) {
|
||||||
menuHandler::homeBaseMenu();
|
menuHandler::homeBaseMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||||
menuHandler::systemBaseMenu();
|
menuHandler::systemBaseMenu();
|
||||||
#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) {
|
||||||
@@ -1411,7 +1608,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
||||||
menuHandler::clockMenu();
|
menuHandler::clockMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||||
menuHandler::LoraRegionPicker();
|
menuHandler::loraMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
|
||||||
if (devicestate.rx_text_message.from) {
|
if (devicestate.rx_text_message.from) {
|
||||||
menuHandler::messageResponseMenu();
|
menuHandler::messageResponseMenu();
|
||||||
@@ -1479,13 +1676,15 @@ bool shouldWakeOnReceivedMessage()
|
|||||||
/*
|
/*
|
||||||
The goal here is to determine when we do NOT wake up the screen on message received:
|
The goal here is to determine when we do NOT wake up the screen on message received:
|
||||||
- Any ext. notifications are turned on
|
- Any ext. notifications are turned on
|
||||||
- If role is not client / client_mute
|
- If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE
|
||||||
- If the battery level is very low
|
- If the battery level is very low
|
||||||
*/
|
*/
|
||||||
if (moduleConfig.external_notification.enabled) {
|
if (moduleConfig.external_notification.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
|
if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT,
|
||||||
|
meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN,
|
||||||
|
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
|
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
|
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||||
|
|
||||||
struct BannerOverlayOptions {
|
struct BannerOverlayOptions {
|
||||||
const char *message;
|
const char *message;
|
||||||
@@ -259,15 +259,7 @@ class Screen : public concurrency::OSThread
|
|||||||
void setup();
|
void setup();
|
||||||
|
|
||||||
/// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink
|
/// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink
|
||||||
void setOn(bool on, FrameCallback einkScreensaver = NULL)
|
void setOn(bool on, FrameCallback einkScreensaver = NULL);
|
||||||
{
|
|
||||||
if (!on)
|
|
||||||
// We handle off commands immediately, because they might be called because the CPU is shutting down
|
|
||||||
handleSetOn(false, einkScreensaver);
|
|
||||||
else
|
|
||||||
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just
|
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just
|
||||||
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
|
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
|
||||||
@@ -315,6 +307,8 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
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 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)
|
void requestMenu(graphics::menuHandler::screenMenus menuToShow)
|
||||||
{
|
{
|
||||||
@@ -593,7 +587,11 @@ class Screen : public concurrency::OSThread
|
|||||||
void setSSLFrames();
|
void setSSLFrames();
|
||||||
|
|
||||||
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
|
||||||
void dismissCurrentFrame();
|
void hideCurrentFrame();
|
||||||
|
|
||||||
|
// Menu-driven Show / Hide Toggle
|
||||||
|
void toggleFrameVisibility(const std::string &frameName);
|
||||||
|
bool isFrameHidden(const std::string &frameName) const;
|
||||||
|
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
/// Draw an image to remain on E-Ink display after screen off
|
/// Draw an image to remain on E-Ink display after screen off
|
||||||
@@ -655,7 +653,7 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t settings = 255;
|
uint8_t settings = 255;
|
||||||
uint8_t wifi = 255;
|
uint8_t wifi = 255;
|
||||||
uint8_t deviceFocused = 255;
|
uint8_t deviceFocused = 255;
|
||||||
uint8_t memory = 255;
|
uint8_t system = 255;
|
||||||
uint8_t gps = 255;
|
uint8_t gps = 255;
|
||||||
uint8_t home = 255;
|
uint8_t home = 255;
|
||||||
uint8_t textMessage = 255;
|
uint8_t textMessage = 255;
|
||||||
@@ -665,6 +663,7 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t nodelist_distance = 255;
|
uint8_t nodelist_distance = 255;
|
||||||
uint8_t nodelist_bearings = 255;
|
uint8_t nodelist_bearings = 255;
|
||||||
uint8_t clock = 255;
|
uint8_t clock = 255;
|
||||||
|
uint8_t chirpy = 255;
|
||||||
uint8_t firstFavorite = 255;
|
uint8_t firstFavorite = 255;
|
||||||
uint8_t lastFavorite = 255;
|
uint8_t lastFavorite = 255;
|
||||||
uint8_t lora = 255;
|
uint8_t lora = 255;
|
||||||
@@ -673,12 +672,29 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t frameCount = 0;
|
uint8_t frameCount = 0;
|
||||||
} framesetInfo;
|
} framesetInfo;
|
||||||
|
|
||||||
struct DismissedFrames {
|
struct hiddenFrames {
|
||||||
bool textMessage = false;
|
bool textMessage = false;
|
||||||
bool waypoint = false;
|
bool waypoint = false;
|
||||||
bool wifi = false;
|
bool wifi = false;
|
||||||
bool memory = false;
|
bool system = false;
|
||||||
} dismissedFrames;
|
bool home = false;
|
||||||
|
bool clock = false;
|
||||||
|
#ifndef USE_EINK
|
||||||
|
bool nodelist = false;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_EINK
|
||||||
|
bool nodelist_lastheard = false;
|
||||||
|
bool nodelist_hopsignal = false;
|
||||||
|
bool nodelist_distance = false;
|
||||||
|
#endif
|
||||||
|
#if HAS_GPS
|
||||||
|
bool nodelist_bearings = false;
|
||||||
|
bool gps = false;
|
||||||
|
#endif
|
||||||
|
bool lora = false;
|
||||||
|
bool show_favorites = false;
|
||||||
|
bool chirpy = true;
|
||||||
|
} hiddenFrames;
|
||||||
|
|
||||||
/// Try to start drawing ASAP
|
/// Try to start drawing ASAP
|
||||||
void setFastFramerate();
|
void setFastFramerate();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/draw/UIRenderer.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "meshtastic/config.pb.h"
|
#include "meshtastic/config.pb.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -16,6 +17,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
|
|||||||
isHighResolution = true;
|
isHighResolution = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (screenwidth > 128 && screenheight <= 64) {
|
||||||
|
isHighResolution = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Special case for Heltec Wireless Tracker v1.1
|
// Special case for Heltec Wireless Tracker v1.1
|
||||||
if (screenwidth == 160 && screenheight == 80) {
|
if (screenwidth == 160 && screenheight == 80) {
|
||||||
isHighResolution = false;
|
isHighResolution = false;
|
||||||
@@ -53,7 +58,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, bool battery_only)
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||||||
{
|
{
|
||||||
constexpr int HEADER_OFFSET_Y = 1;
|
constexpr int HEADER_OFFSET_Y = 1;
|
||||||
y += HEADER_OFFSET_Y;
|
y += HEADER_OFFSET_Y;
|
||||||
@@ -69,7 +74,7 @@ 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();
|
||||||
|
|
||||||
if (!battery_only) {
|
if (!force_no_invert) {
|
||||||
// === Inverted Header Background ===
|
// === Inverted Header Background ===
|
||||||
if (isInverted) {
|
if (isInverted) {
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -187,13 +192,28 @@ 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 && !battery_only) {
|
if (rtc_sec > 0) {
|
||||||
// === 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;
|
||||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||||
|
|
||||||
|
// === Build Date String ===
|
||||||
|
char datetimeStr[25];
|
||||||
|
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||||||
|
char dateLine[40];
|
||||||
|
|
||||||
|
if (isHighResolution) {
|
||||||
|
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||||||
|
} else {
|
||||||
|
if (hasUnreadMessage) {
|
||||||
|
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
|
||||||
|
} else {
|
||||||
|
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.display.use_12h_clock) {
|
if (config.display.use_12h_clock) {
|
||||||
bool isPM = hour >= 12;
|
bool isPM = hour >= 12;
|
||||||
hour %= 12;
|
hour %= 12;
|
||||||
@@ -202,7 +222,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_date) {
|
||||||
|
timeStrWidth = display->getStringWidth(dateLine);
|
||||||
|
} else {
|
||||||
timeStrWidth = display->getStringWidth(timeStr);
|
timeStrWidth = display->getStringWidth(timeStr);
|
||||||
|
}
|
||||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
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 ===
|
||||||
@@ -229,7 +253,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int iconW = 16, iconH = 12;
|
int iconW = 16, iconH = 12;
|
||||||
int iconX = iconRightEdge - iconW;
|
int iconX = iconRightEdge - iconW;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||||
if (isInverted) {
|
if (isInverted && !force_no_invert) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -244,7 +268,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
} else {
|
} else {
|
||||||
int iconX = iconRightEdge - (mail_width - 2);
|
int iconX = iconRightEdge - (mail_width - 2);
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||||
if (isInverted) {
|
if (isInverted && !force_no_invert) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -260,7 +284,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
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;
|
||||||
|
|
||||||
if (isInverted) {
|
if (isInverted && !force_no_invert) {
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
@@ -287,10 +311,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_date) {
|
||||||
|
// === Draw Date ===
|
||||||
|
display->drawString(timeX, textY, dateLine);
|
||||||
|
if (isBold)
|
||||||
|
display->drawString(timeX - 1, textY, dateLine);
|
||||||
|
} else {
|
||||||
// === Draw Time ===
|
// === Draw Time ===
|
||||||
display->drawString(timeX, textY, timeStr);
|
display->drawString(timeX, textY, timeStr);
|
||||||
if (isBold)
|
if (isBold)
|
||||||
display->drawString(timeX - 1, textY, timeStr);
|
display->drawString(timeX - 1, textY, timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ void determineResolution(int16_t screenheight, int16_t screenwidth);
|
|||||||
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 = "", bool battery_only = false);
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||||
|
bool show_date = false);
|
||||||
|
|
||||||
const int *getTextPositions(OLEDDisplay *display);
|
const int *getTextPositions(OLEDDisplay *display);
|
||||||
|
|
||||||
|
|||||||
@@ -751,10 +751,8 @@ 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 "Panel_sdl.hpp"
|
||||||
#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
|
||||||
{
|
{
|
||||||
@@ -767,26 +765,26 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
|
|
||||||
LGFX(void)
|
LGFX(void)
|
||||||
{
|
{
|
||||||
if (settingsMap[displayPanel] == st7789)
|
if (portduino_config.displayPanel == st7789)
|
||||||
_panel_instance = new lgfx::Panel_ST7789;
|
_panel_instance = new lgfx::Panel_ST7789;
|
||||||
else if (settingsMap[displayPanel] == st7735)
|
else if (portduino_config.displayPanel == st7735)
|
||||||
_panel_instance = new lgfx::Panel_ST7735;
|
_panel_instance = new lgfx::Panel_ST7735;
|
||||||
else if (settingsMap[displayPanel] == st7735s)
|
else if (portduino_config.displayPanel == st7735s)
|
||||||
_panel_instance = new lgfx::Panel_ST7735S;
|
_panel_instance = new lgfx::Panel_ST7735S;
|
||||||
else if (settingsMap[displayPanel] == st7796)
|
else if (portduino_config.displayPanel == st7796)
|
||||||
_panel_instance = new lgfx::Panel_ST7796;
|
_panel_instance = new lgfx::Panel_ST7796;
|
||||||
else if (settingsMap[displayPanel] == ili9341)
|
else if (portduino_config.displayPanel == ili9341)
|
||||||
_panel_instance = new lgfx::Panel_ILI9341;
|
_panel_instance = new lgfx::Panel_ILI9341;
|
||||||
else if (settingsMap[displayPanel] == ili9342)
|
else if (portduino_config.displayPanel == ili9342)
|
||||||
_panel_instance = new lgfx::Panel_ILI9342;
|
_panel_instance = new lgfx::Panel_ILI9342;
|
||||||
else if (settingsMap[displayPanel] == ili9488)
|
else if (portduino_config.displayPanel == ili9488)
|
||||||
_panel_instance = new lgfx::Panel_ILI9488;
|
_panel_instance = new lgfx::Panel_ILI9488;
|
||||||
else if (settingsMap[displayPanel] == hx8357d)
|
else if (portduino_config.displayPanel == hx8357d)
|
||||||
_panel_instance = new lgfx::Panel_HX8357D;
|
_panel_instance = new lgfx::Panel_HX8357D;
|
||||||
#if defined(LGFX_SDL)
|
#if defined(SDL_h_)
|
||||||
else if (settingsMap[displayPanel] == x11) {
|
|
||||||
|
else if (portduino_config.displayPanel == x11)
|
||||||
_panel_instance = new lgfx::Panel_sdl;
|
_panel_instance = new lgfx::Panel_sdl;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
else {
|
else {
|
||||||
_panel_instance = new lgfx::Panel_NULL;
|
_panel_instance = new lgfx::Panel_NULL;
|
||||||
@@ -795,61 +793,62 @@ class LGFX : public lgfx::LGFX_Device
|
|||||||
|
|
||||||
auto buscfg = _bus_instance.config();
|
auto buscfg = _bus_instance.config();
|
||||||
buscfg.spi_mode = 0;
|
buscfg.spi_mode = 0;
|
||||||
buscfg.spi_host = settingsMap[displayspidev];
|
buscfg.spi_host = portduino_config.display_spi_dev_int;
|
||||||
|
|
||||||
buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable)
|
buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable)
|
||||||
|
|
||||||
_bus_instance.config(buscfg); // applies the set value to the bus.
|
_bus_instance.config(buscfg); // applies the set value to the bus.
|
||||||
|
if (portduino_config.displayPanel != x11)
|
||||||
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
|
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
|
||||||
|
|
||||||
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
|
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
|
||||||
LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
|
LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight);
|
||||||
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
|
cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable)
|
||||||
cfg.pin_rst = settingsMap[displayReset];
|
cfg.pin_rst = portduino_config.displayReset.pin;
|
||||||
if (settingsMap[displayRotate]) {
|
if (portduino_config.displayRotate) {
|
||||||
cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
|
cfg.panel_width = portduino_config.displayHeight; // actual displayable width
|
||||||
cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
|
cfg.panel_height = portduino_config.displayWidth; // actual displayable height
|
||||||
} else {
|
} else {
|
||||||
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
|
cfg.panel_width = portduino_config.displayWidth; // actual displayable width
|
||||||
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
|
cfg.panel_height = portduino_config.displayHeight; // actual displayable height
|
||||||
}
|
}
|
||||||
cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
|
cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction
|
||||||
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
|
cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction
|
||||||
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored)
|
||||||
cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed
|
cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed
|
||||||
|
|
||||||
_panel_instance->config(cfg);
|
_panel_instance->config(cfg);
|
||||||
|
|
||||||
// Configure settings for touch control.
|
// Configure settings for touch control.
|
||||||
if (settingsMap[touchscreenModule]) {
|
if (portduino_config.touchscreenModule) {
|
||||||
if (settingsMap[touchscreenModule] == xpt2046) {
|
if (portduino_config.touchscreenModule == xpt2046) {
|
||||||
_touch_instance = new lgfx::Touch_XPT2046;
|
_touch_instance = new lgfx::Touch_XPT2046;
|
||||||
} else if (settingsMap[touchscreenModule] == stmpe610) {
|
} else if (portduino_config.touchscreenModule == stmpe610) {
|
||||||
_touch_instance = new lgfx::Touch_STMPE610;
|
_touch_instance = new lgfx::Touch_STMPE610;
|
||||||
} else if (settingsMap[touchscreenModule] == ft5x06) {
|
} else if (portduino_config.touchscreenModule == ft5x06) {
|
||||||
_touch_instance = new lgfx::Touch_FT5x06;
|
_touch_instance = new lgfx::Touch_FT5x06;
|
||||||
}
|
}
|
||||||
auto touch_cfg = _touch_instance->config();
|
auto touch_cfg = _touch_instance->config();
|
||||||
|
|
||||||
touch_cfg.pin_cs = settingsMap[touchscreenCS];
|
touch_cfg.pin_cs = portduino_config.touchscreenCS.pin;
|
||||||
touch_cfg.x_min = 0;
|
touch_cfg.x_min = 0;
|
||||||
touch_cfg.x_max = settingsMap[displayHeight] - 1;
|
touch_cfg.x_max = portduino_config.displayHeight - 1;
|
||||||
touch_cfg.y_min = 0;
|
touch_cfg.y_min = 0;
|
||||||
touch_cfg.y_max = settingsMap[displayWidth] - 1;
|
touch_cfg.y_max = portduino_config.displayWidth - 1;
|
||||||
touch_cfg.pin_int = settingsMap[touchscreenIRQ];
|
touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin;
|
||||||
touch_cfg.bus_shared = true;
|
touch_cfg.bus_shared = true;
|
||||||
touch_cfg.offset_rotation = settingsMap[touchscreenRotate];
|
touch_cfg.offset_rotation = portduino_config.touchscreenRotate;
|
||||||
if (settingsMap[touchscreenI2CAddr] != -1) {
|
if (portduino_config.touchscreenI2CAddr != -1) {
|
||||||
touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr];
|
touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr;
|
||||||
} else {
|
} else {
|
||||||
touch_cfg.spi_host = settingsMap[touchscreenspidev];
|
touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int;
|
||||||
}
|
}
|
||||||
|
|
||||||
_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 defined(SDL_h_)
|
||||||
if (settingsMap[displayPanel] == x11) {
|
if (portduino_config.displayPanel == x11) {
|
||||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
|
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
|
||||||
sdl_panel_->setup();
|
sdl_panel_->setup();
|
||||||
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
|
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
|
||||||
@@ -1115,10 +1114,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
|
|||||||
backlightEnable = p;
|
backlightEnable = p;
|
||||||
|
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
if (settingsMap[displayRotate]) {
|
if (portduino_config.displayRotate) {
|
||||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
|
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth);
|
||||||
} else {
|
} else {
|
||||||
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
|
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(SCREEN_ROTATE)
|
#elif defined(SCREEN_ROTATE)
|
||||||
@@ -1237,37 +1236,36 @@ void TFTDisplay::display(bool fromBlank)
|
|||||||
|
|
||||||
void TFTDisplay::sdlLoop()
|
void TFTDisplay::sdlLoop()
|
||||||
{
|
{
|
||||||
#if defined(LGFX_SDL)
|
#if defined(SDL_h_)
|
||||||
static int lastPressed = 0;
|
static int lastPressed = 0;
|
||||||
static int shuttingDown = false;
|
static int shuttingDown = false;
|
||||||
if (settingsMap[displayPanel] == x11) {
|
if (portduino_config.displayPanel == x11) {
|
||||||
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
|
||||||
if (sdl_panel_->loop() && !shuttingDown) {
|
if (sdl_panel_->loop() && !shuttingDown) {
|
||||||
LOG_WARN("Window Closed!");
|
LOG_WARN("Window Closed!");
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// debounce
|
// debounce
|
||||||
if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
|
if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed))
|
||||||
return;
|
return;
|
||||||
if (!lgfx::v1::gpio_in(37)) {
|
if (!sdl_panel_->gpio_in(37)) {
|
||||||
lastPressed = 37;
|
lastPressed = 37;
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
} else if (!lgfx::v1::gpio_in(36)) {
|
} else if (!sdl_panel_->gpio_in(36)) {
|
||||||
lastPressed = 36;
|
lastPressed = 36;
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
} else if (!lgfx::v1::gpio_in(38)) {
|
} else if (!sdl_panel_->gpio_in(38)) {
|
||||||
lastPressed = 38;
|
lastPressed = 38;
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
} else if (!lgfx::v1::gpio_in(39)) {
|
} else if (!sdl_panel_->gpio_in(39)) {
|
||||||
lastPressed = 39;
|
lastPressed = 39;
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
} else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) {
|
} else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) {
|
||||||
lastPressed = SDL_SCANCODE_KP_ENTER;
|
lastPressed = SDL_SCANCODE_KP_ENTER;
|
||||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||||
inputBroker->injectInputEvent(&event);
|
inputBroker->injectInputEvent(&event);
|
||||||
@@ -1288,8 +1286,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
|||||||
backlightEnable->set(true);
|
backlightEnable->set(true);
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
display(true);
|
display(true);
|
||||||
if (settingsMap[displayBacklight] > 0)
|
if (portduino_config.displayBacklight.pin > 0)
|
||||||
digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON);
|
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
|
||||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||||
tft->wakeup();
|
tft->wakeup();
|
||||||
tft->powerSaveOff();
|
tft->powerSaveOff();
|
||||||
@@ -1312,8 +1310,8 @@ void TFTDisplay::sendCommand(uint8_t com)
|
|||||||
backlightEnable->set(false);
|
backlightEnable->set(false);
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
tft->clear();
|
tft->clear();
|
||||||
if (settingsMap[displayBacklight] > 0)
|
if (portduino_config.displayBacklight.pin > 0)
|
||||||
digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON);
|
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
|
||||||
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
|
||||||
tft->sleep();
|
tft->sleep();
|
||||||
tft->powerSaveOn();
|
tft->powerSaveOn();
|
||||||
|
|||||||
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
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/draw/UIRenderer.h"
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
@@ -190,7 +191,8 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
// === Set Title, Blank for Clock
|
// === Set Title, Blank for Clock
|
||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||||
|
int line = 0;
|
||||||
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
@@ -294,6 +296,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
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 = (isHighResolution) ? 18 : 10;
|
xOffset = (isHighResolution) ? 18 : 10;
|
||||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||||
@@ -313,7 +316,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
// === Set Title, Blank for Clock
|
// === Set Title, Blank for Clock
|
||||||
const char *titleStr = "";
|
const char *titleStr = "";
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||||
|
int line = 0;
|
||||||
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
|
|||||||
@@ -330,8 +330,7 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
// Line 3
|
// Line 3
|
||||||
if (config.display.gps_format !=
|
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
|
||||||
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
|
|
||||||
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
||||||
|
|
||||||
// Line 4
|
// Line 4
|
||||||
@@ -395,8 +394,18 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int textWidth = display->getStringWidth(shortnameble);
|
int textWidth = display->getStringWidth(shortnameble);
|
||||||
int nameX = (SCREEN_WIDTH - textWidth);
|
int nameX = (SCREEN_WIDTH - textWidth);
|
||||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||||
// === Second Row: Radio Preset ===
|
|
||||||
|
// === Second Row: Role ===
|
||||||
|
auto role = DisplayFormatters::getDeviceRole(config.device.role);
|
||||||
|
char device_role[25];
|
||||||
|
snprintf(device_role, sizeof(device_role), "Role: %s", role);
|
||||||
|
textWidth = display->getStringWidth(device_role);
|
||||||
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
|
display->drawString(nameX, getTextPositions(display)[line++], device_role);
|
||||||
|
|
||||||
|
// === Third Row: Radio Preset ===
|
||||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
||||||
|
|
||||||
char regionradiopreset[25];
|
char regionradiopreset[25];
|
||||||
const char *region = myRegion ? myRegion->name : NULL;
|
const char *region = myRegion ? myRegion->name : NULL;
|
||||||
if (region != nullptr) {
|
if (region != nullptr) {
|
||||||
@@ -410,7 +419,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
|
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
|
||||||
|
|
||||||
// === Third Row: Frequency / ChanNum ===
|
// === Fourth Row: Frequency / ChanNum ===
|
||||||
char frequencyslot[35];
|
char frequencyslot[35];
|
||||||
char freqStr[16];
|
char freqStr[16];
|
||||||
float freq = RadioLibInterface::instance->getFreq();
|
float freq = RadioLibInterface::instance->getFreq();
|
||||||
@@ -437,7 +446,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
||||||
|
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === Fourth Row: Channel Utilization ===
|
// === Fifth Row: Channel Utilization ===
|
||||||
const char *chUtil = "ChUtil:";
|
const char *chUtil = "ChUtil:";
|
||||||
char chUtilPercentage[10];
|
char chUtilPercentage[10];
|
||||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
@@ -454,7 +463,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||||
int starting_position = centerofscreen - total_line_content_width;
|
int starting_position = centerofscreen - total_line_content_width;
|
||||||
|
|
||||||
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
|
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
||||||
|
|
||||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||||
if (chutil_percent >= 61) {
|
if (chutil_percent >= 61) {
|
||||||
@@ -491,7 +500,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
|
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
||||||
chUtilPercentage);
|
chUtilPercentage);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -499,7 +508,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
// ****************************
|
// ****************************
|
||||||
// * System Screen *
|
// * System Screen *
|
||||||
// ****************************
|
// ****************************
|
||||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->clear();
|
display->clear();
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
@@ -655,6 +664,34 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * Chirpy Screen *
|
||||||
|
// ****************************
|
||||||
|
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
|
display->clear();
|
||||||
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int line = 1;
|
||||||
|
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||||
|
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||||
|
int textX_offset = 10;
|
||||||
|
if (isHighResolution) {
|
||||||
|
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
|
||||||
|
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
|
||||||
|
textX_offset = textX_offset * 4;
|
||||||
|
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||||
|
}
|
||||||
|
|
||||||
|
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||||
|
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||||
|
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||||
|
display->drawString(textX, getTextPositions(display)[line++], "World!");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace DebugRenderer
|
} // namespace DebugRenderer
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
#endif
|
#endif
|
||||||
@@ -31,8 +31,10 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state
|
|||||||
// LoRa information display
|
// LoRa information display
|
||||||
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
// Memory screen display
|
// System screen display
|
||||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
// Chirpy screen display
|
||||||
|
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
} // namespace DebugRenderer
|
} // namespace DebugRenderer
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/draw/UIRenderer.h"
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
|
#include "input/UpDownInterruptImpl1.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "mesh/MeshTypes.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
#include "modules/CannedMessageModule.h"
|
#include "modules/CannedMessageModule.h"
|
||||||
#include "modules/KeyVerificationModule.h"
|
#include "modules/KeyVerificationModule.h"
|
||||||
@@ -26,6 +29,28 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
|||||||
bool test_enabled = false;
|
bool test_enabled = false;
|
||||||
uint8_t test_count = 0;
|
uint8_t test_count = 0;
|
||||||
|
|
||||||
|
void menuHandler::loraMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
|
||||||
|
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "LoRa Actions";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 4;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == Back) {
|
||||||
|
// No action
|
||||||
|
} else if (selected == device_role_picker) {
|
||||||
|
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||||
|
} else if (selected == radio_preset_picker) {
|
||||||
|
menuHandler::menuQueue = menuHandler::radio_preset_picker;
|
||||||
|
} else if (selected == lora_picker) {
|
||||||
|
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::OnboardMessage()
|
void menuHandler::OnboardMessage()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"OK", "Got it!"};
|
static const char *optionsArray[] = {"OK", "Got it!"};
|
||||||
@@ -91,6 +116,8 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
||||||
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
||||||
|
auto changes = SEGMENT_CONFIG;
|
||||||
|
|
||||||
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
||||||
if (!owner.is_licensed) {
|
if (!owner.is_licensed) {
|
||||||
bool keygenSuccess = false;
|
bool keygenSuccess = false;
|
||||||
@@ -99,6 +126,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||||
keygenSuccess = true;
|
keygenSuccess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Generate new PKI keys");
|
LOG_INFO("Generate new PKI keys");
|
||||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||||
@@ -116,13 +144,101 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
|||||||
if (myRegion->dutyCycle < 100) {
|
if (myRegion->dutyCycle < 100) {
|
||||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||||
}
|
}
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
|
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||||
|
// Default broker is in use, so subscribe to the appropriate MQTT root topic for this region
|
||||||
|
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||||
|
changes |= SEGMENT_MODULECONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
service->reloadConfig(changes);
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::DeviceRolePicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||||
|
enum optionsNumbers {
|
||||||
|
Back = 0,
|
||||||
|
devicerole_client = 1,
|
||||||
|
devicerole_clientmute = 2,
|
||||||
|
devicerole_lostandfound = 3,
|
||||||
|
devicerole_tracker = 4
|
||||||
|
};
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Device Role";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 5;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == Back) {
|
||||||
|
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||||
|
screen->runNow();
|
||||||
|
return;
|
||||||
|
} else if (selected == devicerole_client) {
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||||
|
} else if (selected == devicerole_clientmute) {
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
|
||||||
|
} else if (selected == devicerole_lostandfound) {
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
|
||||||
|
} else if (selected == devicerole_tracker) {
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::RadioPresetPicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
|
||||||
|
"MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
|
||||||
|
enum optionsNumbers {
|
||||||
|
Back = 0,
|
||||||
|
radiopreset_LongSlow = 1,
|
||||||
|
radiopreset_LongModerate = 2,
|
||||||
|
radiopreset_LongFast = 3,
|
||||||
|
radiopreset_MediumSlow = 4,
|
||||||
|
radiopreset_MediumFast = 5,
|
||||||
|
radiopreset_ShortSlow = 6,
|
||||||
|
radiopreset_ShortFast = 7,
|
||||||
|
radiopreset_ShortTurbo = 8
|
||||||
|
};
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Radio Preset";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 9;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == Back) {
|
||||||
|
menuHandler::menuQueue = menuHandler::lora_Menu;
|
||||||
|
screen->runNow();
|
||||||
|
return;
|
||||||
|
} else if (selected == radiopreset_LongSlow) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
|
||||||
|
} else if (selected == radiopreset_LongModerate) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
|
||||||
|
} else if (selected == radiopreset_LongFast) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||||
|
} else if (selected == radiopreset_MediumSlow) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
|
||||||
|
} else if (selected == radiopreset_MediumFast) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
||||||
|
} else if (selected == radiopreset_ShortSlow) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
|
||||||
|
} else if (selected == radiopreset_ShortFast) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
|
||||||
|
} else if (selected == radiopreset_ShortTurbo) {
|
||||||
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::TwelveHourPicker()
|
void menuHandler::TwelveHourPicker()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||||
@@ -320,7 +436,7 @@ void menuHandler::messageResponseMenu()
|
|||||||
bannerOptions.optionsCount = options;
|
bannerOptions.optionsCount = options;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == Dismiss) {
|
if (selected == Dismiss) {
|
||||||
screen->dismissCurrentFrame();
|
screen->hideCurrentFrame();
|
||||||
} else if (selected == Preset) {
|
} else if (selected == Preset) {
|
||||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||||
@@ -361,8 +477,11 @@ void menuHandler::homeBaseMenu()
|
|||||||
optionsArray[options] = "Sleep Screen";
|
optionsArray[options] = "Sleep Screen";
|
||||||
optionsEnumArray[options++] = Sleep;
|
optionsEnumArray[options++] = Sleep;
|
||||||
#endif
|
#endif
|
||||||
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
optionsArray[options] = "Send Position";
|
optionsArray[options] = "Send Position";
|
||||||
|
} else {
|
||||||
|
optionsArray[options] = "Send Node Info";
|
||||||
|
}
|
||||||
optionsEnumArray[options++] = Position;
|
optionsEnumArray[options++] = Position;
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
optionsArray[options] = "New Preset";
|
optionsArray[options] = "New Preset";
|
||||||
@@ -455,7 +574,7 @@ void menuHandler::textMessageBaseMenu()
|
|||||||
|
|
||||||
void menuHandler::systemBaseMenu()
|
void menuHandler::systemBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
|
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd };
|
||||||
static const char *optionsArray[enumEnd] = {"Back"};
|
static const char *optionsArray[enumEnd] = {"Back"};
|
||||||
static int optionsEnumArray[enumEnd] = {Back};
|
static int optionsEnumArray[enumEnd] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
@@ -467,6 +586,9 @@ void menuHandler::systemBaseMenu()
|
|||||||
optionsArray[options] = "Screen Options";
|
optionsArray[options] = "Screen Options";
|
||||||
optionsEnumArray[options++] = ScreenOptions;
|
optionsEnumArray[options++] = ScreenOptions;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
optionsArray[options] = "Frame Visiblity Toggle";
|
||||||
|
optionsEnumArray[options++] = FrameToggles;
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
optionsArray[options] = "Bluetooth";
|
optionsArray[options] = "Bluetooth";
|
||||||
#else
|
#else
|
||||||
@@ -504,6 +626,9 @@ void menuHandler::systemBaseMenu()
|
|||||||
} else if (selected == PowerMenu) {
|
} else if (selected == PowerMenu) {
|
||||||
menuHandler::menuQueue = menuHandler::power_menu;
|
menuHandler::menuQueue = menuHandler::power_menu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == FrameToggles) {
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
} else if (selected == Test) {
|
} else if (selected == Test) {
|
||||||
menuHandler::menuQueue = menuHandler::test_menu;
|
menuHandler::menuQueue = menuHandler::test_menu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
@@ -570,16 +695,17 @@ void menuHandler::favoriteBaseMenu()
|
|||||||
|
|
||||||
void menuHandler::positionBaseMenu()
|
void menuHandler::positionBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd };
|
enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd };
|
||||||
|
|
||||||
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"};
|
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"};
|
||||||
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu};
|
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu};
|
||||||
int options = 3;
|
int options = 4;
|
||||||
|
|
||||||
if (accelerometerThread) {
|
if (accelerometerThread) {
|
||||||
optionsArray[options] = "Compass Calibrate";
|
optionsArray[options] = "Compass Calibrate";
|
||||||
optionsEnumArray[options++] = CompassCalibrate;
|
optionsEnumArray[options++] = CompassCalibrate;
|
||||||
}
|
}
|
||||||
|
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "Position Action";
|
bannerOptions.message = "Position Action";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
@@ -589,6 +715,9 @@ void menuHandler::positionBaseMenu()
|
|||||||
if (selected == GPSToggle) {
|
if (selected == GPSToggle) {
|
||||||
menuQueue = gps_toggle_menu;
|
menuQueue = gps_toggle_menu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == GPSFormat) {
|
||||||
|
menuQueue = gps_format_menu;
|
||||||
|
screen->runNow();
|
||||||
} else if (selected == CompassMenu) {
|
} else if (selected == CompassMenu) {
|
||||||
menuQueue = compass_point_north_menu;
|
menuQueue = compass_point_north_menu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
@@ -633,6 +762,31 @@ void menuHandler::nodeListMenu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::nodeNameLengthMenu()
|
||||||
|
{
|
||||||
|
enum OptionsNumbers { Back, Long, Short };
|
||||||
|
static const char *optionsArray[] = {"Back", "Long", "Short"};
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Node Name Length";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 3;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == Long) {
|
||||||
|
// Set names to long
|
||||||
|
LOG_INFO("Setting names to long");
|
||||||
|
config.display.use_long_node_name = true;
|
||||||
|
} else if (selected == Short) {
|
||||||
|
// Set names to short
|
||||||
|
LOG_INFO("Setting names to short");
|
||||||
|
config.display.use_long_node_name = false;
|
||||||
|
} else if (selected == Back) {
|
||||||
|
menuQueue = screen_options_menu;
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::resetNodeDBMenu()
|
void menuHandler::resetNodeDBMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||||
@@ -715,6 +869,58 @@ void menuHandler::GPSToggleMenu()
|
|||||||
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
|
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
void menuHandler::GPSFormatMenu()
|
||||||
|
{
|
||||||
|
|
||||||
|
static const char *optionsArray[] = {"Back",
|
||||||
|
isHighResolution ? "Decimal Degrees" : "DEC",
|
||||||
|
isHighResolution ? "Degrees Minutes Seconds" : "DMS",
|
||||||
|
isHighResolution ? "Universal Transverse Mercator" : "UTM",
|
||||||
|
isHighResolution ? "Military Grid Reference System" : "MGRS",
|
||||||
|
isHighResolution ? "Open Location Code" : "OLC",
|
||||||
|
isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
|
||||||
|
isHighResolution ? "Maidenhead Locator" : "MLS"};
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "GPS Format";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = 8;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 3) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 4) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 5) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 6) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 7) {
|
||||||
|
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
|
||||||
|
saveUIConfig();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else {
|
||||||
|
menuQueue = position_base_menu;
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bannerOptions.InitialSelected = uiconfig.gps_format + 1;
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void menuHandler::BluetoothToggleMenu()
|
void menuHandler::BluetoothToggleMenu()
|
||||||
@@ -740,11 +946,11 @@ void menuHandler::BluetoothToggleMenu()
|
|||||||
|
|
||||||
void menuHandler::BuzzerModeMenu()
|
void menuHandler::BuzzerModeMenu()
|
||||||
{
|
{
|
||||||
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
|
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"};
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
bannerOptions.message = "Buzzer Mode";
|
bannerOptions.message = "Buzzer Mode";
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
bannerOptions.optionsCount = 4;
|
bannerOptions.optionsCount = 5;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
@@ -1010,16 +1216,33 @@ void menuHandler::traceRouteMenu()
|
|||||||
void menuHandler::testMenu()
|
void menuHandler::testMenu()
|
||||||
{
|
{
|
||||||
|
|
||||||
static const char *optionsArray[] = {"Back", "Number Picker"};
|
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
|
||||||
|
static const char *optionsArray[4] = {"Back"};
|
||||||
|
static int optionsEnumArray[4] = {Back};
|
||||||
|
int options = 1;
|
||||||
|
|
||||||
|
optionsArray[options] = "Number Picker";
|
||||||
|
optionsEnumArray[options++] = NumberPicker;
|
||||||
|
|
||||||
|
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||||
|
optionsEnumArray[options++] = ShowChirpy;
|
||||||
|
|
||||||
BannerOverlayOptions bannerOptions;
|
BannerOverlayOptions bannerOptions;
|
||||||
std::string message = "Test to Run?\n";
|
bannerOptions.message = "Hidden Test Menu";
|
||||||
bannerOptions.message = message.c_str();
|
|
||||||
bannerOptions.optionsArrayPtr = optionsArray;
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
bannerOptions.optionsCount = 2;
|
bannerOptions.optionsCount = options;
|
||||||
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == 1) {
|
if (selected == NumberPicker) {
|
||||||
menuQueue = number_test;
|
menuQueue = number_test;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == ShowChirpy) {
|
||||||
|
screen->toggleFrameVisibility("chirpy");
|
||||||
|
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
menuQueue = system_base_menu;
|
||||||
|
screen->runNow();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
@@ -1106,11 +1329,16 @@ void menuHandler::screenOptionsMenu()
|
|||||||
hasSupportBrightness = false;
|
hasSupportBrightness = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
enum optionsNumbers { Back, Brightness, ScreenColor };
|
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor };
|
||||||
static const char *optionsArray[4] = {"Back"};
|
static const char *optionsArray[5] = {"Back"};
|
||||||
static int optionsEnumArray[4] = {Back};
|
static int optionsEnumArray[5] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
|
#if defined(T_DECK) || defined(T_LORA_PAGER)
|
||||||
|
optionsArray[options] = "Show Long/Short Name";
|
||||||
|
optionsEnumArray[options++] = NodeNameLength;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Only show brightness for B&W displays
|
// Only show brightness for B&W displays
|
||||||
if (hasSupportBrightness) {
|
if (hasSupportBrightness) {
|
||||||
optionsArray[options] = "Brightness";
|
optionsArray[options] = "Brightness";
|
||||||
@@ -1135,6 +1363,9 @@ void menuHandler::screenOptionsMenu()
|
|||||||
} else if (selected == ScreenColor) {
|
} else if (selected == ScreenColor) {
|
||||||
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
menuHandler::menuQueue = menuHandler::tftcolormenupicker;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
|
} else if (selected == NodeNameLength) {
|
||||||
|
menuHandler::menuQueue = menuHandler::node_name_length_menu;
|
||||||
|
screen->runNow();
|
||||||
} else {
|
} else {
|
||||||
menuQueue = system_base_menu;
|
menuQueue = system_base_menu;
|
||||||
screen->runNow();
|
screen->runNow();
|
||||||
@@ -1220,6 +1451,132 @@ void menuHandler::keyVerificationFinalPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::FrameToggles_menu()
|
||||||
|
{
|
||||||
|
enum optionsNumbers {
|
||||||
|
Finish,
|
||||||
|
nodelist,
|
||||||
|
nodelist_lastheard,
|
||||||
|
nodelist_hopsignal,
|
||||||
|
nodelist_distance,
|
||||||
|
nodelist_bearings,
|
||||||
|
gps,
|
||||||
|
lora,
|
||||||
|
clock,
|
||||||
|
show_favorites,
|
||||||
|
show_telemetry,
|
||||||
|
show_power,
|
||||||
|
enumEnd
|
||||||
|
};
|
||||||
|
static const char *optionsArray[enumEnd] = {"Finish"};
|
||||||
|
static int optionsEnumArray[enumEnd] = {Finish};
|
||||||
|
int options = 1;
|
||||||
|
|
||||||
|
// Track last selected index (not enum value!)
|
||||||
|
static int lastSelectedIndex = 0;
|
||||||
|
|
||||||
|
#ifndef USE_EINK
|
||||||
|
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
|
||||||
|
optionsEnumArray[options++] = nodelist;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_EINK
|
||||||
|
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
|
||||||
|
optionsEnumArray[options++] = nodelist_lastheard;
|
||||||
|
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
|
||||||
|
optionsEnumArray[options++] = nodelist_hopsignal;
|
||||||
|
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
|
||||||
|
optionsEnumArray[options++] = nodelist_distance;
|
||||||
|
#endif
|
||||||
|
#if HAS_GPS
|
||||||
|
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
|
||||||
|
optionsEnumArray[options++] = nodelist_bearings;
|
||||||
|
|
||||||
|
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
|
||||||
|
optionsEnumArray[options++] = gps;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
|
||||||
|
optionsEnumArray[options++] = lora;
|
||||||
|
|
||||||
|
optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
|
||||||
|
optionsEnumArray[options++] = clock;
|
||||||
|
|
||||||
|
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
|
||||||
|
optionsEnumArray[options++] = show_favorites;
|
||||||
|
|
||||||
|
optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry";
|
||||||
|
optionsEnumArray[options++] = show_telemetry;
|
||||||
|
|
||||||
|
optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power";
|
||||||
|
optionsEnumArray[options++] = show_power;
|
||||||
|
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Show/Hide Frames";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsCount = options;
|
||||||
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
|
bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value
|
||||||
|
|
||||||
|
bannerOptions.bannerCallback = [options](int selected) mutable -> void {
|
||||||
|
// Find the index of selected in optionsEnumArray
|
||||||
|
int idx = 0;
|
||||||
|
for (; idx < options; ++idx) {
|
||||||
|
if (optionsEnumArray[idx] == selected)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastSelectedIndex = idx;
|
||||||
|
|
||||||
|
if (selected == Finish) {
|
||||||
|
screen->setFrames(Screen::FOCUS_DEFAULT);
|
||||||
|
} else if (selected == nodelist) {
|
||||||
|
screen->toggleFrameVisibility("nodelist");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == nodelist_lastheard) {
|
||||||
|
screen->toggleFrameVisibility("nodelist_lastheard");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == nodelist_hopsignal) {
|
||||||
|
screen->toggleFrameVisibility("nodelist_hopsignal");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == nodelist_distance) {
|
||||||
|
screen->toggleFrameVisibility("nodelist_distance");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == nodelist_bearings) {
|
||||||
|
screen->toggleFrameVisibility("nodelist_bearings");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == gps) {
|
||||||
|
screen->toggleFrameVisibility("gps");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == lora) {
|
||||||
|
screen->toggleFrameVisibility("lora");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == clock) {
|
||||||
|
screen->toggleFrameVisibility("clock");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == show_favorites) {
|
||||||
|
screen->toggleFrameVisibility("show_favorites");
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == show_telemetry) {
|
||||||
|
moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled;
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
} else if (selected == show_power) {
|
||||||
|
moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled;
|
||||||
|
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||||
{
|
{
|
||||||
if (menuQueue != menu_none)
|
if (menuQueue != menu_none)
|
||||||
@@ -1227,9 +1584,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
switch (menuQueue) {
|
switch (menuQueue) {
|
||||||
case menu_none:
|
case menu_none:
|
||||||
break;
|
break;
|
||||||
|
case lora_Menu:
|
||||||
|
loraMenu();
|
||||||
|
break;
|
||||||
case lora_picker:
|
case lora_picker:
|
||||||
LoraRegionPicker();
|
LoraRegionPicker();
|
||||||
break;
|
break;
|
||||||
|
case device_role_picker:
|
||||||
|
DeviceRolePicker();
|
||||||
|
break;
|
||||||
|
case radio_preset_picker:
|
||||||
|
RadioPresetPicker();
|
||||||
|
break;
|
||||||
case no_timeout_lora_picker:
|
case no_timeout_lora_picker:
|
||||||
LoraRegionPicker(0);
|
LoraRegionPicker(0);
|
||||||
break;
|
break;
|
||||||
@@ -1255,6 +1621,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case gps_toggle_menu:
|
case gps_toggle_menu:
|
||||||
GPSToggleMenu();
|
GPSToggleMenu();
|
||||||
break;
|
break;
|
||||||
|
case gps_format_menu:
|
||||||
|
GPSFormatMenu();
|
||||||
|
break;
|
||||||
#endif
|
#endif
|
||||||
case compass_point_north_menu:
|
case compass_point_north_menu:
|
||||||
compassNorthMenu();
|
compassNorthMenu();
|
||||||
@@ -1274,6 +1643,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case brightness_picker:
|
case brightness_picker:
|
||||||
BrightnessPickerMenu();
|
BrightnessPickerMenu();
|
||||||
break;
|
break;
|
||||||
|
case node_name_length_menu:
|
||||||
|
nodeNameLengthMenu();
|
||||||
|
break;
|
||||||
case reboot_menu:
|
case reboot_menu:
|
||||||
rebootMenu();
|
rebootMenu();
|
||||||
break;
|
break;
|
||||||
@@ -1316,6 +1688,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case power_menu:
|
case power_menu:
|
||||||
powerMenu();
|
powerMenu();
|
||||||
break;
|
break;
|
||||||
|
case FrameToggles:
|
||||||
|
FrameToggles_menu();
|
||||||
|
break;
|
||||||
case throttle_message:
|
case throttle_message:
|
||||||
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ class menuHandler
|
|||||||
public:
|
public:
|
||||||
enum screenMenus {
|
enum screenMenus {
|
||||||
menu_none,
|
menu_none,
|
||||||
|
lora_Menu,
|
||||||
lora_picker,
|
lora_picker,
|
||||||
|
device_role_picker,
|
||||||
|
radio_preset_picker,
|
||||||
no_timeout_lora_picker,
|
no_timeout_lora_picker,
|
||||||
TZ_picker,
|
TZ_picker,
|
||||||
twelve_hour_picker,
|
twelve_hour_picker,
|
||||||
@@ -17,6 +20,7 @@ class menuHandler
|
|||||||
clock_menu,
|
clock_menu,
|
||||||
position_base_menu,
|
position_base_menu,
|
||||||
gps_toggle_menu,
|
gps_toggle_menu,
|
||||||
|
gps_format_menu,
|
||||||
compass_point_north_menu,
|
compass_point_north_menu,
|
||||||
reset_node_db_menu,
|
reset_node_db_menu,
|
||||||
buzzermodemenupicker,
|
buzzermodemenupicker,
|
||||||
@@ -39,11 +43,16 @@ class menuHandler
|
|||||||
key_verification_final_prompt,
|
key_verification_final_prompt,
|
||||||
trace_route_menu,
|
trace_route_menu,
|
||||||
throttle_message,
|
throttle_message,
|
||||||
|
node_name_length_menu,
|
||||||
|
FrameToggles
|
||||||
};
|
};
|
||||||
static screenMenus menuQueue;
|
static screenMenus menuQueue;
|
||||||
|
|
||||||
static void OnboardMessage();
|
static void OnboardMessage();
|
||||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||||
|
static void loraMenu();
|
||||||
|
static void DeviceRolePicker();
|
||||||
|
static void RadioPresetPicker();
|
||||||
static void handleMenuSwitch(OLEDDisplay *display);
|
static void handleMenuSwitch(OLEDDisplay *display);
|
||||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||||
static void clockMenu();
|
static void clockMenu();
|
||||||
@@ -58,6 +67,7 @@ class menuHandler
|
|||||||
static void positionBaseMenu();
|
static void positionBaseMenu();
|
||||||
static void compassNorthMenu();
|
static void compassNorthMenu();
|
||||||
static void GPSToggleMenu();
|
static void GPSToggleMenu();
|
||||||
|
static void GPSFormatMenu();
|
||||||
static void BuzzerModeMenu();
|
static void BuzzerModeMenu();
|
||||||
static void switchToMUIMenu();
|
static void switchToMUIMenu();
|
||||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||||
@@ -76,6 +86,8 @@ class menuHandler
|
|||||||
static void notificationsMenu();
|
static void notificationsMenu();
|
||||||
static void screenOptionsMenu();
|
static void screenOptionsMenu();
|
||||||
static void powerMenu();
|
static void powerMenu();
|
||||||
|
static void nodeNameLengthMenu();
|
||||||
|
static void FrameToggles_menu();
|
||||||
static void textMessageMenu();
|
static void textMessageMenu();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -55,26 +55,32 @@ static int scrollIndex = 0;
|
|||||||
|
|
||||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
||||||
{
|
{
|
||||||
|
const char *name = NULL;
|
||||||
static char nodeName[16] = "?";
|
static char nodeName[16] = "?";
|
||||||
|
if (config.display.use_long_node_name == true) {
|
||||||
|
if (node->has_user && strlen(node->user.long_name) > 0) {
|
||||||
|
name = node->user.long_name;
|
||||||
|
} else {
|
||||||
|
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (node->has_user && strlen(node->user.short_name) > 0) {
|
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||||
bool valid = true;
|
name = node->user.short_name;
|
||||||
const char *name = node->user.short_name;
|
} else {
|
||||||
for (size_t i = 0; i < strlen(name); i++) {
|
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||||
uint8_t c = (uint8_t)name[i];
|
|
||||||
if (c < 32 || c > 126) {
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (valid) {
|
|
||||||
strncpy(nodeName, name, sizeof(nodeName) - 1);
|
// Use sanitizeString() function and copy directly into nodeName
|
||||||
|
std::string sanitized_name = sanitizeString(name ? name : "");
|
||||||
|
|
||||||
|
if (!sanitized_name.empty()) {
|
||||||
|
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
|
||||||
nodeName[sizeof(nodeName) - 1] = '\0';
|
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||||
} else {
|
} else {
|
||||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
|
||||||
}
|
|
||||||
return nodeName;
|
return nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,18 @@
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/images.h"
|
#include "graphics/images.h"
|
||||||
|
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||||
|
#include "input/UpDownInterruptImpl1.h"
|
||||||
|
#if HAS_BUTTON
|
||||||
|
#include "input/ButtonThread.h"
|
||||||
|
#endif
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#if HAS_TRACKBALL
|
||||||
|
#include "input/TrackballInterruptImpl1.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
@@ -18,6 +26,11 @@
|
|||||||
|
|
||||||
using namespace meshtastic;
|
using namespace meshtastic;
|
||||||
|
|
||||||
|
#if HAS_BUTTON
|
||||||
|
// Global button thread pointer defined in main.cpp
|
||||||
|
extern ::ButtonThread *UserButtonThread;
|
||||||
|
#endif
|
||||||
|
|
||||||
// External references to global variables from Screen.cpp
|
// External references to global variables from Screen.cpp
|
||||||
extern std::vector<std::string> functionSymbol;
|
extern std::vector<std::string> functionSymbol;
|
||||||
extern std::string functionSymbolString;
|
extern std::string functionSymbolString;
|
||||||
@@ -38,6 +51,8 @@ bool NotificationRenderer::pauseBanner = false;
|
|||||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||||
uint32_t NotificationRenderer::numDigits = 0;
|
uint32_t NotificationRenderer::numDigits = 0;
|
||||||
uint32_t NotificationRenderer::currentNumber = 0;
|
uint32_t NotificationRenderer::currentNumber = 0;
|
||||||
|
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
|
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
|
||||||
uint32_t pow_of_10(uint32_t n)
|
uint32_t pow_of_10(uint32_t n)
|
||||||
{
|
{
|
||||||
@@ -89,14 +104,33 @@ void NotificationRenderer::resetBanner()
|
|||||||
|
|
||||||
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
{
|
{
|
||||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
// Handle text_input notifications first - they have their own timeout/banner logic
|
||||||
|
if (current_notification_type == notificationTypeEnum::text_input) {
|
||||||
|
// Check for timeout and reset if needed for text input
|
||||||
|
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||||
resetBanner();
|
resetBanner();
|
||||||
if (!isOverlayBannerShowing() || pauseBanner)
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
drawTextInput(display, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||||
|
resetBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit if no banner is showing or banner is paused
|
||||||
|
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (current_notification_type) {
|
switch (current_notification_type) {
|
||||||
case notificationTypeEnum::none:
|
case notificationTypeEnum::none:
|
||||||
// Do nothing - no notification to display
|
// Do nothing - no notification to display
|
||||||
break;
|
break;
|
||||||
|
case notificationTypeEnum::text_input:
|
||||||
|
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
|
||||||
|
break;
|
||||||
case notificationTypeEnum::text_banner:
|
case notificationTypeEnum::text_banner:
|
||||||
case notificationTypeEnum::selection_picker:
|
case notificationTypeEnum::selection_picker:
|
||||||
drawAlertBannerOverlay(display, state);
|
drawAlertBannerOverlay(display, state);
|
||||||
@@ -216,9 +250,11 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle input
|
// Handle input
|
||||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
|
||||||
|
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
curSelected--;
|
curSelected--;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
|
||||||
|
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
curSelected++;
|
curSelected++;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||||
alertBannerCallback(selectedNodenum);
|
alertBannerCallback(selectedNodenum);
|
||||||
@@ -267,12 +303,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
|||||||
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
|
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
|
||||||
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
|
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
|
||||||
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
|
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
||||||
}
|
}
|
||||||
// make temp buffer for name
|
|
||||||
// fi
|
|
||||||
if (i == curSelected) {
|
if (i == curSelected) {
|
||||||
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
||||||
if (isHighResolution) {
|
if (isHighResolution) {
|
||||||
@@ -286,7 +319,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
|||||||
}
|
}
|
||||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||||
} else {
|
} else {
|
||||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
|
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
|
||||||
|
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||||
}
|
}
|
||||||
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
||||||
}
|
}
|
||||||
@@ -333,9 +367,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
|||||||
|
|
||||||
// Handle input
|
// Handle input
|
||||||
if (alertBannerOptions > 0) {
|
if (alertBannerOptions > 0) {
|
||||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
|
||||||
|
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
curSelected--;
|
curSelected--;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
|
||||||
|
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
curSelected++;
|
curSelected++;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||||
if (optionsEnumPtr != nullptr) {
|
if (optionsEnumPtr != nullptr) {
|
||||||
@@ -705,6 +741,99 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
|||||||
"Please be patient and do not power off.");
|
"Please be patient and do not power off.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
|
{
|
||||||
|
if (virtualKeyboard) {
|
||||||
|
// Check for timeout and auto-exit if needed
|
||||||
|
if (virtualKeyboard->isTimedOut()) {
|
||||||
|
LOG_INFO("Virtual keyboard timeout - auto-exiting");
|
||||||
|
// Cancel virtual keyboard - call callback with empty string to indicate timeout
|
||||||
|
auto callback = textInputCallback; // Store callback before clearing
|
||||||
|
|
||||||
|
// Clean up first to prevent re-entry
|
||||||
|
delete virtualKeyboard;
|
||||||
|
virtualKeyboard = nullptr;
|
||||||
|
textInputCallback = nullptr;
|
||||||
|
resetBanner();
|
||||||
|
|
||||||
|
// Call callback after cleanup
|
||||||
|
if (callback) {
|
||||||
|
callback("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore normal overlays
|
||||||
|
if (screen) {
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||||
|
if (inEvent.inputEvent == INPUT_BROKER_UP) {
|
||||||
|
// high frequency for move cursor left/right than up/down with encoders
|
||||||
|
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||||
|
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||||
|
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||||
|
virtualKeyboard->moveCursorLeft();
|
||||||
|
} else {
|
||||||
|
virtualKeyboard->moveCursorUp();
|
||||||
|
}
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
|
||||||
|
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||||
|
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
|
||||||
|
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
|
||||||
|
virtualKeyboard->moveCursorRight();
|
||||||
|
} else {
|
||||||
|
virtualKeyboard->moveCursorDown();
|
||||||
|
}
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||||
|
virtualKeyboard->moveCursorLeft();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||||
|
virtualKeyboard->moveCursorRight();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||||
|
virtualKeyboard->moveCursorUp();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||||
|
virtualKeyboard->moveCursorDown();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
|
virtualKeyboard->moveCursorLeft();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
|
virtualKeyboard->moveCursorRight();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
|
||||||
|
virtualKeyboard->handlePress();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
|
||||||
|
virtualKeyboard->handleLongPress();
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
|
||||||
|
auto callback = textInputCallback;
|
||||||
|
delete virtualKeyboard;
|
||||||
|
virtualKeyboard = nullptr;
|
||||||
|
textInputCallback = nullptr;
|
||||||
|
resetBanner();
|
||||||
|
if (callback) {
|
||||||
|
callback("");
|
||||||
|
}
|
||||||
|
if (screen) {
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the event after processing for virtual keyboard
|
||||||
|
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the screen to avoid overlapping with underlying frames or overlays
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||||
|
display->setColor(WHITE);
|
||||||
|
// Draw the virtual keyboard
|
||||||
|
virtualKeyboard->draw(display, 0, 0);
|
||||||
|
} else {
|
||||||
|
// If virtualKeyboard is null, reset the banner to avoid getting stuck
|
||||||
|
LOG_INFO("Virtual keyboard is null - resetting banner");
|
||||||
|
resetBanner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool NotificationRenderer::isOverlayBannerShowing()
|
bool NotificationRenderer::isOverlayBannerShowing()
|
||||||
{
|
{
|
||||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#include "OLEDDisplay.h"
|
#include "OLEDDisplay.h"
|
||||||
#include "OLEDDisplayUi.h"
|
#include "OLEDDisplayUi.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
|
#include "graphics/VirtualKeyboard.h"
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
#define MAX_LINES 5
|
#define MAX_LINES 5
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -22,6 +25,8 @@ class NotificationRenderer
|
|||||||
static std::function<void(int)> alertBannerCallback;
|
static std::function<void(int)> alertBannerCallback;
|
||||||
static uint32_t numDigits;
|
static uint32_t numDigits;
|
||||||
static uint32_t currentNumber;
|
static uint32_t currentNumber;
|
||||||
|
static VirtualKeyboard *virtualKeyboard;
|
||||||
|
static std::function<void(const std::string &)> textInputCallback;
|
||||||
|
|
||||||
static bool pauseBanner;
|
static bool pauseBanner;
|
||||||
|
|
||||||
@@ -30,6 +35,7 @@ class NotificationRenderer
|
|||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
|
|
||||||
// External variables
|
// External variables
|
||||||
extern graphics::Screen *screen;
|
extern graphics::Screen *screen;
|
||||||
|
#if defined(M5STACK_UNITC6L)
|
||||||
static uint32_t lastSwitchTime = 0;
|
static uint32_t lastSwitchTime = 0;
|
||||||
|
#endif
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
||||||
@@ -116,64 +118,124 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw GPS status coordinates
|
// Draw GPS status coordinates
|
||||||
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
|
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps,
|
||||||
|
const char *mode)
|
||||||
{
|
{
|
||||||
auto gpsFormat = config.display.gps_format;
|
auto gpsFormat = uiconfig.gps_format;
|
||||||
char displayLine[32];
|
char displayLine[32];
|
||||||
|
|
||||||
if (!gps->getIsConnected() && !config.position.fixed_position) {
|
if (!gps->getIsConnected() && !config.position.fixed_position) {
|
||||||
|
if (strcmp(mode, "line1") == 0) {
|
||||||
strcpy(displayLine, "No GPS present");
|
strcpy(displayLine, "No GPS present");
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
|
display->drawString(x, y, displayLine);
|
||||||
|
}
|
||||||
} else if (!gps->getHasLock() && !config.position.fixed_position) {
|
} else if (!gps->getHasLock() && !config.position.fixed_position) {
|
||||||
|
if (strcmp(mode, "line1") == 0) {
|
||||||
strcpy(displayLine, "No GPS Lock");
|
strcpy(displayLine, "No GPS Lock");
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
|
display->drawString(x, y, displayLine);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
|
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
|
||||||
|
|
||||||
if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) {
|
if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) {
|
||||||
char coordinateLine[22];
|
char coordinateLine_1[22];
|
||||||
if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
|
char coordinateLine_2[22];
|
||||||
snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7,
|
if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
|
||||||
geoCoord.getLongitude() * 1e-7);
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7);
|
||||||
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
|
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7);
|
||||||
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(),
|
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
|
||||||
geoCoord.getUTMEasting(), geoCoord.getUTMNorthing());
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(),
|
||||||
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
|
geoCoord.getUTMBand(), geoCoord.getUTMEasting());
|
||||||
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(),
|
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing());
|
||||||
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(),
|
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
|
||||||
geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing());
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(),
|
||||||
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code
|
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k());
|
||||||
geoCoord.getOLCCode(coordinateLine);
|
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(),
|
||||||
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
|
geoCoord.getMGRSNorthing());
|
||||||
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region
|
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code
|
||||||
snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary");
|
geoCoord.getOLCCode(coordinateLine_1);
|
||||||
else
|
coordinateLine_2[0] = '\0';
|
||||||
snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(),
|
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
|
||||||
geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing());
|
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region
|
||||||
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary");
|
||||||
|
coordinateLine_2[0] = '\0';
|
||||||
|
} else {
|
||||||
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(),
|
||||||
|
geoCoord.getOSGRN100k());
|
||||||
|
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(),
|
||||||
|
geoCoord.getOSGRNorthing());
|
||||||
|
}
|
||||||
|
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System
|
||||||
|
double lat = geoCoord.getLatitude() * 1e-7;
|
||||||
|
double lon = geoCoord.getLongitude() * 1e-7;
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
if (lat > 90.0)
|
||||||
|
lat = 90.0;
|
||||||
|
if (lat < -90.0)
|
||||||
|
lat = -90.0;
|
||||||
|
while (lon < -180.0)
|
||||||
|
lon += 360.0;
|
||||||
|
while (lon >= 180.0)
|
||||||
|
lon -= 360.0;
|
||||||
|
|
||||||
|
double adjLon = lon + 180.0;
|
||||||
|
double adjLat = lat + 90.0;
|
||||||
|
|
||||||
|
char maiden[10]; // enough for 8-char + null
|
||||||
|
|
||||||
|
// Field (2 letters)
|
||||||
|
int lonField = int(adjLon / 20.0);
|
||||||
|
int latField = int(adjLat / 10.0);
|
||||||
|
adjLon -= lonField * 20.0;
|
||||||
|
adjLat -= latField * 10.0;
|
||||||
|
|
||||||
|
// Square (2 digits)
|
||||||
|
int lonSquare = int(adjLon / 2.0);
|
||||||
|
int latSquare = int(adjLat / 1.0);
|
||||||
|
adjLon -= lonSquare * 2.0;
|
||||||
|
adjLat -= latSquare * 1.0;
|
||||||
|
|
||||||
|
// Subsquare (2 letters)
|
||||||
|
double lonUnit = 2.0 / 24.0;
|
||||||
|
double latUnit = 1.0 / 24.0;
|
||||||
|
int lonSub = int(adjLon / lonUnit);
|
||||||
|
int latSub = int(adjLat / latUnit);
|
||||||
|
|
||||||
|
snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare,
|
||||||
|
'A' + lonSub, 'A' + latSub);
|
||||||
|
|
||||||
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden);
|
||||||
|
coordinateLine_2[0] = '\0'; // only need one line
|
||||||
}
|
}
|
||||||
|
|
||||||
// If fixed position, display text "Fixed GPS" alternating with the coordinates.
|
if (strcmp(mode, "line1") == 0) {
|
||||||
if (config.position.fixed_position) {
|
display->drawString(x, y, coordinateLine_1);
|
||||||
if ((millis() / 10000) % 2) {
|
} else if (strcmp(mode, "line2") == 0) {
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y,
|
display->drawString(x, y, coordinateLine_2);
|
||||||
coordinateLine);
|
} else if (strcmp(mode, "combined") == 0) {
|
||||||
} else {
|
display->drawString(x, y, coordinateLine_1);
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS");
|
if (coordinateLine_2[0] != '\0') {
|
||||||
|
display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
char latLine[22];
|
char coordinateLine_1[22];
|
||||||
char lonLine[22];
|
char coordinateLine_2[22];
|
||||||
snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(),
|
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(),
|
||||||
geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
|
geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
|
||||||
snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(),
|
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(),
|
||||||
geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
|
geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1,
|
if (strcmp(mode, "line1") == 0) {
|
||||||
latLine);
|
display->drawString(x, y, coordinateLine_1);
|
||||||
display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine);
|
} else if (strcmp(mode, "line2") == 0) {
|
||||||
|
display->drawString(x, y, coordinateLine_2);
|
||||||
|
} else { // both
|
||||||
|
display->drawString(x, y, coordinateLine_1);
|
||||||
|
display->drawString(x, y + 10, coordinateLine_2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,9 +291,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
||||||
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
||||||
return;
|
return;
|
||||||
uint32_t now = millis();
|
|
||||||
display->clear();
|
display->clear();
|
||||||
#if defined(M5STACK_UNITC6L)
|
#if defined(M5STACK_UNITC6L)
|
||||||
|
uint32_t now = millis();
|
||||||
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
|
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
|
||||||
{
|
{
|
||||||
display->display();
|
display->display();
|
||||||
@@ -676,7 +738,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
int textWidth = 0;
|
int textWidth = 0;
|
||||||
int nameX = 0;
|
int nameX = 0;
|
||||||
int yOffset = (isHighResolution) ? 0 : 5;
|
int yOffset = (isHighResolution) ? 0 : 5;
|
||||||
const char *longName = nullptr;
|
|
||||||
std::string longNameStr;
|
std::string longNameStr;
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
@@ -937,7 +998,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
config.display.heading_bold = false;
|
config.display.heading_bold = false;
|
||||||
|
|
||||||
const char *displayLine = ""; // Initialize to empty string by default
|
const char *displayLine = ""; // Initialize to empty string by default
|
||||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
|
|
||||||
|
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
|
||||||
|
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
|
||||||
|
|
||||||
|
if (usePhoneGPS) {
|
||||||
|
// Phone-provided GPS is active
|
||||||
|
displayLine = "Phone GPS";
|
||||||
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
|
if (isHighResolution) {
|
||||||
|
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||||
|
imgSatellite_height, imgSatellite, display);
|
||||||
|
} else {
|
||||||
|
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||||
|
imgSatellite);
|
||||||
|
}
|
||||||
|
int xOffset = (isHighResolution) ? 6 : 0;
|
||||||
|
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||||
|
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||||
|
// GPS disabled / not present
|
||||||
if (config.position.fixed_position) {
|
if (config.position.fixed_position) {
|
||||||
displayLine = "Fixed GPS";
|
displayLine = "Fixed GPS";
|
||||||
} else {
|
} else {
|
||||||
@@ -954,6 +1034,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
int xOffset = (isHighResolution) ? 6 : 0;
|
int xOffset = (isHighResolution) ? 6 : 0;
|
||||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||||
} else {
|
} else {
|
||||||
|
// Onboard GPS
|
||||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,46 +1061,62 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
|
|
||||||
// If GPS is off, no need to display these parts
|
// If GPS is off, no need to display these parts
|
||||||
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
|
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
|
||||||
|
// === Second Row: Last GPS Fix ===
|
||||||
|
if (gpsStatus->getLastFixMillis() > 0) {
|
||||||
|
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
|
||||||
|
uint32_t days = delta / 86400;
|
||||||
|
uint32_t hours = (delta % 86400) / 3600;
|
||||||
|
uint32_t mins = (delta % 3600) / 60;
|
||||||
|
uint32_t secs = delta % 60;
|
||||||
|
|
||||||
// === Second Row: Date ===
|
char buf[32];
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
#if defined(USE_EINK)
|
||||||
char datetimeStr[25];
|
// E-Ink: skip seconds, show only days/hours/mins
|
||||||
bool showTime = false; // set to true for full datetime
|
if (days > 0) {
|
||||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
|
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
|
||||||
char fullLine[40];
|
} else if (hours > 0) {
|
||||||
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
|
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
|
||||||
#if !defined(M5STACK_UNITC6L)
|
|
||||||
display->drawString(0, getTextPositions(display)[line++], fullLine);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// === Third Row: Latitude ===
|
|
||||||
char latStr[32];
|
|
||||||
#if defined(M5STACK_UNITC6L)
|
|
||||||
snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7);
|
|
||||||
display->drawString(x, getTextPositions(display)[line++] + 2, latStr);
|
|
||||||
#else
|
|
||||||
snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
|
|
||||||
display->drawString(x, getTextPositions(display)[line++], latStr);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// === Fourth Row: Longitude ===
|
|
||||||
char lonStr[32];
|
|
||||||
#if defined(M5STACK_UNITC6L)
|
|
||||||
snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7);
|
|
||||||
display->drawString(x, getTextPositions(display)[line++] + 4, lonStr);
|
|
||||||
#else
|
|
||||||
snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
|
|
||||||
display->drawString(x, getTextPositions(display)[line++], lonStr);
|
|
||||||
|
|
||||||
// === Fifth Row: Altitude ===
|
|
||||||
char DisplayLineTwo[32] = {0};
|
|
||||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
|
||||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
|
|
||||||
} else {
|
} else {
|
||||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
|
snprintf(buf, sizeof(buf), "Last: %um", mins);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Non E-Ink: include seconds where useful
|
||||||
|
if (days > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
|
||||||
|
} else if (hours > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
|
||||||
|
} else if (mins > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "Last: %us", secs);
|
||||||
}
|
}
|
||||||
display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
display->drawString(0, getTextPositions(display)[line++], buf);
|
||||||
|
} else {
|
||||||
|
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Third Row: Line 1 GPS Info ===
|
||||||
|
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
|
||||||
|
|
||||||
|
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
|
||||||
|
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
|
||||||
|
// === Fourth Row: Line 2 GPS Info ===
|
||||||
|
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Final Row: Altitude ===
|
||||||
|
char altitudeLine[32] = {0};
|
||||||
|
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||||
|
? ourNode->position.altitude
|
||||||
|
: geoCoord.getAltitude();
|
||||||
|
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||||
|
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
|
||||||
|
} else {
|
||||||
|
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
||||||
|
}
|
||||||
|
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
||||||
}
|
}
|
||||||
#if !defined(M5STACK_UNITC6L)
|
#if !defined(M5STACK_UNITC6L)
|
||||||
// === Draw Compass if heading is valid ===
|
// === Draw Compass if heading is valid ===
|
||||||
@@ -1104,7 +1201,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif // HAS_GPS
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USERPREFS_OEM_TEXT
|
#ifdef USERPREFS_OEM_TEXT
|
||||||
@@ -1197,14 +1294,13 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
||||||
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
||||||
|
|
||||||
// Only show bar briefly after switching frames
|
|
||||||
static uint32_t navBarLastShown = 0;
|
|
||||||
static bool cosmeticRefreshDone = false;
|
|
||||||
|
|
||||||
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
|
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
|
||||||
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
|
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
|
||||||
|
|
||||||
#if defined(USE_EINK)
|
#if defined(USE_EINK)
|
||||||
|
// Only show bar briefly after switching frames
|
||||||
|
static uint32_t navBarLastShown = 0;
|
||||||
|
static bool cosmeticRefreshDone = false;
|
||||||
static bool navBarPrevVisible = false;
|
static bool navBarPrevVisible = false;
|
||||||
|
|
||||||
if (navBarVisible && !navBarPrevVisible) {
|
if (navBarVisible && !navBarPrevVisible) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "NodeDB.h"
|
||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
@@ -37,7 +38,8 @@ class UIRenderer
|
|||||||
|
|
||||||
// GPS status functions
|
// GPS status functions
|
||||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus,
|
||||||
|
const char *mode = "line1");
|
||||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = {
|
|||||||
0xA9 // Row 7: #..#.#.#
|
0xA9 // Row 7: #..#.#.#
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🪙 Memory Icon
|
// 🪙 System Icon
|
||||||
const uint8_t icon_memory[] PROGMEM = {
|
const uint8_t icon_system[] PROGMEM = {
|
||||||
0x24, // Row 0: ..#..#..
|
0x24, // Row 0: ..#..#..
|
||||||
0x3C, // Row 1: ..####..
|
0x3C, // Row 1: ..####..
|
||||||
0xC3, // Row 2: ##....##
|
0xC3, // Row 2: ##....##
|
||||||
@@ -287,6 +287,79 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101
|
|||||||
#define analog_icon_clock_height 8
|
#define analog_icon_clock_height 8
|
||||||
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
||||||
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||||
|
|
||||||
|
#define chirpy_width 38
|
||||||
|
#define chirpy_height 50
|
||||||
|
const uint8_t chirpy[] = {
|
||||||
|
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
|
||||||
|
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
|
||||||
|
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
|
||||||
|
0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
|
||||||
|
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
|
||||||
|
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1,
|
||||||
|
0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff,
|
||||||
|
0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3,
|
||||||
|
0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03,
|
||||||
|
0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0,
|
||||||
|
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||||
|
|
||||||
|
#define chirpy_width_hirez 76
|
||||||
|
#define chirpy_height_hirez 100
|
||||||
|
const uint8_t chirpy_hirez[] = {
|
||||||
|
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
|
||||||
|
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
|
||||||
|
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
|
||||||
|
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||||
|
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||||
|
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||||
|
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
|
||||||
|
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
|
||||||
|
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
|
||||||
|
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
|
||||||
|
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
|
||||||
|
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
|
||||||
|
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
|
||||||
|
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
|
||||||
|
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
|
||||||
|
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
|
||||||
|
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
|
||||||
|
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
|
||||||
|
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
|
||||||
|
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
|
||||||
|
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
|
||||||
|
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
|
||||||
|
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
|
||||||
|
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
|
||||||
|
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
|
||||||
|
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
|
||||||
|
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
|
||||||
|
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
|
||||||
|
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
|
||||||
|
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
|
||||||
|
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
|
||||||
|
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
|
||||||
|
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
|
||||||
|
|
||||||
|
#define chirpy_small_image_width 8
|
||||||
|
#define chirpy_small_image_height 8
|
||||||
|
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||||
|
|
||||||
#ifdef M5STACK_UNITC6L
|
#ifdef M5STACK_UNITC6L
|
||||||
#include "img/icon_small.xbm"
|
#include "img/icon_small.xbm"
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -41,78 +41,78 @@ void tftSetup(void)
|
|||||||
PacketAPI::create(PacketServer::init());
|
PacketAPI::create(PacketServer::init());
|
||||||
deviceScreen->init(new PacketClient);
|
deviceScreen->init(new PacketClient);
|
||||||
#else
|
#else
|
||||||
if (settingsMap[displayPanel] != no_screen) {
|
if (portduino_config.displayPanel != no_screen) {
|
||||||
DisplayDriverConfig displayConfig;
|
DisplayDriverConfig displayConfig;
|
||||||
static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S",
|
static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S",
|
||||||
"ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
|
"ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
|
||||||
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
|
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
|
||||||
#if defined(USE_X11)
|
#if defined(USE_X11)
|
||||||
if (settingsMap[displayPanel] == x11) {
|
if (portduino_config.displayPanel == x11) {
|
||||||
if (settingsMap[displayWidth] && settingsMap[displayHeight])
|
if (portduino_config.displayWidth && portduino_config.displayHeight)
|
||||||
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth],
|
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth,
|
||||||
(uint16_t)settingsMap[displayHeight]);
|
(uint16_t)portduino_config.displayHeight);
|
||||||
else
|
else
|
||||||
displayConfig.device(DisplayDriverConfig::device_t::X11);
|
displayConfig.device(DisplayDriverConfig::device_t::X11);
|
||||||
} else
|
} else
|
||||||
#elif defined(USE_FRAMEBUFFER)
|
#elif defined(USE_FRAMEBUFFER)
|
||||||
if (settingsMap[displayPanel] == fb) {
|
if (portduino_config.displayPanel == fb) {
|
||||||
if (settingsMap[displayWidth] && settingsMap[displayHeight])
|
if (portduino_config.displayWidth && portduino_config.displayHeight)
|
||||||
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth],
|
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth,
|
||||||
(uint16_t)settingsMap[displayHeight]);
|
(uint16_t)portduino_config.displayHeight);
|
||||||
else
|
else
|
||||||
displayConfig.device(DisplayDriverConfig::device_t::FB);
|
displayConfig.device(DisplayDriverConfig::device_t::FB);
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
|
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
|
||||||
.panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]],
|
.panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel],
|
||||||
.panel_width = (uint16_t)settingsMap[displayWidth],
|
.panel_width = (uint16_t)portduino_config.displayWidth,
|
||||||
.panel_height = (uint16_t)settingsMap[displayHeight],
|
.panel_height = (uint16_t)portduino_config.displayHeight,
|
||||||
.rotation = (bool)settingsMap[displayRotate],
|
.rotation = (bool)portduino_config.displayRotate,
|
||||||
.pin_cs = (int16_t)settingsMap[displayCS],
|
.pin_cs = (int16_t)portduino_config.displayCS.pin,
|
||||||
.pin_rst = (int16_t)settingsMap[displayReset],
|
.pin_rst = (int16_t)portduino_config.displayReset.pin,
|
||||||
.offset_x = (uint16_t)settingsMap[displayOffsetX],
|
.offset_x = (uint16_t)portduino_config.displayOffsetX,
|
||||||
.offset_y = (uint16_t)settingsMap[displayOffsetY],
|
.offset_y = (uint16_t)portduino_config.displayOffsetY,
|
||||||
.offset_rotation = (uint8_t)settingsMap[displayOffsetRotate],
|
.offset_rotation = (uint8_t)portduino_config.displayOffsetRotate,
|
||||||
.invert = settingsMap[displayInvert] ? true : false,
|
.invert = portduino_config.displayInvert ? true : false,
|
||||||
.rgb_order = (bool)settingsMap[displayRGBOrder],
|
.rgb_order = (bool)portduino_config.displayRGBOrder,
|
||||||
.dlen_16bit = settingsMap[displayPanel] == ili9486 ||
|
.dlen_16bit = portduino_config.displayPanel == ili9486 ||
|
||||||
settingsMap[displayPanel] == ili9488})
|
portduino_config.displayPanel == ili9488})
|
||||||
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency],
|
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency,
|
||||||
.freq_read = 16000000,
|
.freq_read = 16000000,
|
||||||
.spi{.pin_dc = (int8_t)settingsMap[displayDC],
|
.spi{.pin_dc = (int8_t)portduino_config.displayDC.pin,
|
||||||
.use_lock = true,
|
.use_lock = true,
|
||||||
.spi_host = (uint16_t)settingsMap[displayspidev]}})
|
.spi_host = (uint16_t)portduino_config.display_spi_dev_int}})
|
||||||
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice],
|
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice,
|
||||||
.pointerDevice = settingsStrings[pointerDevice]})
|
.pointerDevice = portduino_config.pointerDevice})
|
||||||
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight],
|
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin,
|
||||||
.pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel],
|
.pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin,
|
||||||
.invert = (bool)settingsMap[displayBacklightInvert]});
|
.invert = (bool)portduino_config.displayBacklightInvert});
|
||||||
if (settingsMap[touchscreenI2CAddr] == -1) {
|
if (portduino_config.touchscreenI2CAddr == -1) {
|
||||||
displayConfig.touch(
|
displayConfig.touch(
|
||||||
DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]],
|
DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule],
|
||||||
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
|
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
|
||||||
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
|
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
|
||||||
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
|
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
|
||||||
.spi{
|
.spi{
|
||||||
.spi_host = (int8_t)settingsMap[touchscreenspidev],
|
.spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int,
|
||||||
},
|
},
|
||||||
.pin_cs = (int16_t)settingsMap[touchscreenCS]});
|
.pin_cs = (int16_t)portduino_config.touchscreenCS.pin});
|
||||||
} else {
|
} else {
|
||||||
displayConfig.touch(DisplayDriverConfig::touch_config_t{
|
displayConfig.touch(DisplayDriverConfig::touch_config_t{
|
||||||
.type = touch[settingsMap[touchscreenModule]],
|
.type = touch[portduino_config.touchscreenModule],
|
||||||
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
|
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
|
||||||
.x_min = 0,
|
.x_min = 0,
|
||||||
.x_max =
|
.x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth
|
||||||
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) -
|
: portduino_config.displayHeight) -
|
||||||
1),
|
1),
|
||||||
.y_min = 0,
|
.y_min = 0,
|
||||||
.y_max =
|
.y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight
|
||||||
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
|
: portduino_config.displayWidth) -
|
||||||
1),
|
1),
|
||||||
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
|
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
|
||||||
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
|
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
|
||||||
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
|
.i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceScreen = &DeviceScreen::create(&displayConfig);
|
deviceScreen = &DeviceScreen::create(&displayConfig);
|
||||||
|
|||||||
@@ -274,7 +274,12 @@ int32_t ButtonThread::runOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
btnEvent = BUTTON_EVENT_NONE;
|
btnEvent = BUTTON_EVENT_NONE;
|
||||||
|
|
||||||
|
// only pull when the button is pressed, we get notified via IRQ on a new press
|
||||||
|
if (!userButton.isIdle() || waitingForLongPress) {
|
||||||
return 50;
|
return 50;
|
||||||
|
}
|
||||||
|
return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here?
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ class ButtonThread : public Observable<const InputEvent *>, public concurrency::
|
|||||||
return digitalRead(buttonPin); // Most buttons are active low by default
|
return digitalRead(buttonPin); // Most buttons are active low by default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true while this thread's button is physically held down
|
||||||
|
bool isHeld() { return isButtonPressed(_pinNum); }
|
||||||
|
|
||||||
// Disconnect and reconnect interrupts for light sleep
|
// Disconnect and reconnect interrupts for light sleep
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
int beforeLightSleep(void *unused);
|
int beforeLightSleep(void *unused);
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length)
|
|||||||
// Feed input to the canned messages module
|
// Feed input to the canned messages module
|
||||||
void ExpressLRSFiveWay::sendKey(input_broker_event key)
|
void ExpressLRSFiveWay::sendKey(input_broker_event key)
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.source = inputSourceName;
|
e.source = inputSourceName;
|
||||||
e.inputEvent = key;
|
e.inputEvent = key;
|
||||||
notifyObservers(&e);
|
notifyObservers(&e);
|
||||||
|
|||||||
@@ -3,16 +3,66 @@
|
|||||||
|
|
||||||
InputBroker *inputBroker = nullptr;
|
InputBroker *inputBroker = nullptr;
|
||||||
|
|
||||||
InputBroker::InputBroker(){};
|
InputBroker::InputBroker()
|
||||||
|
{
|
||||||
|
#ifdef HAS_FREE_RTOS
|
||||||
|
inputEventQueue = xQueueCreate(5, sizeof(InputEvent));
|
||||||
|
pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *));
|
||||||
|
xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void InputBroker::registerSource(Observable<const InputEvent *> *source)
|
void InputBroker::registerSource(Observable<const InputEvent *> *source)
|
||||||
{
|
{
|
||||||
this->inputEventObserver.observe(source);
|
this->inputEventObserver.observe(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_FREE_RTOS
|
||||||
|
void InputBroker::requestPollSoon(InputPollable *pollable)
|
||||||
|
{
|
||||||
|
if (xPortInIsrContext() == pdTRUE) {
|
||||||
|
xQueueSendFromISR(pollSoonQueue, &pollable, NULL);
|
||||||
|
} else {
|
||||||
|
xQueueSend(pollSoonQueue, &pollable, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputBroker::queueInputEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
|
if (xPortInIsrContext() == pdTRUE) {
|
||||||
|
xQueueSendFromISR(inputEventQueue, event, NULL);
|
||||||
|
} else {
|
||||||
|
xQueueSend(inputEventQueue, event, portMAX_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputBroker::processInputEventQueue()
|
||||||
|
{
|
||||||
|
InputEvent event;
|
||||||
|
while (xQueueReceive(inputEventQueue, &event, 0)) {
|
||||||
|
handleInputEvent(&event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int InputBroker::handleInputEvent(const InputEvent *event)
|
int InputBroker::handleInputEvent(const InputEvent *event)
|
||||||
{
|
{
|
||||||
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
|
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
|
||||||
this->notifyObservers(event);
|
this->notifyObservers(event);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_FREE_RTOS
|
||||||
|
void InputBroker::pollSoonWorker(void *p)
|
||||||
|
{
|
||||||
|
InputBroker *instance = (InputBroker *)p;
|
||||||
|
while (true) {
|
||||||
|
InputPollable *pollable = NULL;
|
||||||
|
xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY);
|
||||||
|
if (pollable) {
|
||||||
|
pollable->pollOnce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "Observer.h"
|
||||||
|
#include "freertosinc.h"
|
||||||
|
|
||||||
enum input_broker_event {
|
enum input_broker_event {
|
||||||
INPUT_BROKER_NONE = 0,
|
INPUT_BROKER_NONE = 0,
|
||||||
INPUT_BROKER_SELECT = 10,
|
INPUT_BROKER_SELECT = 10,
|
||||||
|
INPUT_BROKER_SELECT_LONG = 11,
|
||||||
|
INPUT_BROKER_UP_LONG = 12,
|
||||||
|
INPUT_BROKER_DOWN_LONG = 13,
|
||||||
INPUT_BROKER_UP = 17,
|
INPUT_BROKER_UP = 17,
|
||||||
INPUT_BROKER_DOWN = 18,
|
INPUT_BROKER_DOWN = 18,
|
||||||
INPUT_BROKER_LEFT = 19,
|
INPUT_BROKER_LEFT = 19,
|
||||||
@@ -38,6 +43,13 @@ typedef struct _InputEvent {
|
|||||||
uint16_t touchX;
|
uint16_t touchX;
|
||||||
uint16_t touchY;
|
uint16_t touchY;
|
||||||
} InputEvent;
|
} InputEvent;
|
||||||
|
|
||||||
|
class InputPollable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void pollOnce() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class InputBroker : public Observable<const InputEvent *>
|
class InputBroker : public Observable<const InputEvent *>
|
||||||
{
|
{
|
||||||
CallbackObserver<InputBroker, const InputEvent *> inputEventObserver =
|
CallbackObserver<InputBroker, const InputEvent *> inputEventObserver =
|
||||||
@@ -47,9 +59,22 @@ class InputBroker : public Observable<const InputEvent *>
|
|||||||
InputBroker();
|
InputBroker();
|
||||||
void registerSource(Observable<const InputEvent *> *source);
|
void registerSource(Observable<const InputEvent *> *source);
|
||||||
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
|
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
|
||||||
|
#ifdef HAS_FREE_RTOS
|
||||||
|
void requestPollSoon(InputPollable *pollable);
|
||||||
|
void queueInputEvent(const InputEvent *event);
|
||||||
|
void processInputEventQueue();
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int handleInputEvent(const InputEvent *event);
|
int handleInputEvent(const InputEvent *event);
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef HAS_FREE_RTOS
|
||||||
|
QueueHandle_t inputEventQueue;
|
||||||
|
QueueHandle_t pollSoonQueue;
|
||||||
|
TaskHandle_t pollSoonTask;
|
||||||
|
static void pollSoonWorker(void *p);
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
extern InputBroker *inputBroker;
|
extern InputBroker *inputBroker;
|
||||||
@@ -33,9 +33,9 @@ int32_t LinuxInput::runOnce()
|
|||||||
{
|
{
|
||||||
|
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
if (settingsStrings[keyboardDevice] == "")
|
if (portduino_config.keyboardDevice == "")
|
||||||
return disable();
|
return disable();
|
||||||
fd = open(settingsStrings[keyboardDevice].c_str(), O_RDWR);
|
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
|
||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
return disable();
|
return disable();
|
||||||
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
ret = ioctl(fd, EVIOCGRAB, (void *)1);
|
||||||
@@ -73,7 +73,7 @@ int32_t LinuxInput::runOnce()
|
|||||||
int rd = read(events[i].data.fd, ev, sizeof(ev));
|
int rd = read(events[i].data.fd, ev, sizeof(ev));
|
||||||
assert(rd > ((signed int)sizeof(struct input_event)));
|
assert(rd > ((signed int)sizeof(struct input_event)));
|
||||||
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
e.kbchar = 0;
|
e.kbchar = 0;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
RotaryEncoderImpl *rotaryEncoderImpl;
|
RotaryEncoderImpl *rotaryEncoderImpl;
|
||||||
|
|
||||||
RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME)
|
RotaryEncoderImpl::RotaryEncoderImpl()
|
||||||
{
|
{
|
||||||
rotary = nullptr;
|
rotary = nullptr;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@ bool RotaryEncoderImpl::init()
|
|||||||
if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 ||
|
if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 ||
|
||||||
moduleConfig.canned_message.inputbroker_pin_b == 0) {
|
moduleConfig.canned_message.inputbroker_pin_b == 0) {
|
||||||
// Input device is disabled.
|
// Input device is disabled.
|
||||||
disable();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +29,11 @@ bool RotaryEncoderImpl::init()
|
|||||||
moduleConfig.canned_message.inputbroker_pin_press);
|
moduleConfig.canned_message.inputbroker_pin_press);
|
||||||
rotary->resetButton();
|
rotary->resetButton();
|
||||||
|
|
||||||
inputBroker->registerSource(this);
|
interruptInstance = this;
|
||||||
|
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
|
||||||
|
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
|
||||||
|
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
|
||||||
|
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
|
||||||
|
|
||||||
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
|
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
|
||||||
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
|
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
|
||||||
@@ -38,36 +41,36 @@ bool RotaryEncoderImpl::init()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t RotaryEncoderImpl::runOnce()
|
void RotaryEncoderImpl::pollOnce()
|
||||||
{
|
{
|
||||||
InputEvent e{originName, INPUT_BROKER_NONE, 0, 0, 0};
|
InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0};
|
||||||
|
|
||||||
static uint32_t lastPressed = millis();
|
static uint32_t lastPressed = millis();
|
||||||
if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
|
if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
|
||||||
if (lastPressed + 200 < millis()) {
|
if (lastPressed + 200 < millis()) {
|
||||||
LOG_DEBUG("Rotary event Press");
|
LOG_DEBUG("Rotary event Press");
|
||||||
lastPressed = millis();
|
lastPressed = millis();
|
||||||
e.inputEvent = this->eventPressed;
|
e.inputEvent = this->eventPressed;
|
||||||
|
inputBroker->queueInputEvent(&e);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
switch (rotary->process()) {
|
switch (rotary->process()) {
|
||||||
case RotaryEncoder::DIRECTION_CW:
|
case RotaryEncoder::DIRECTION_CW:
|
||||||
LOG_DEBUG("Rotary event CW");
|
LOG_DEBUG("Rotary event CW");
|
||||||
e.inputEvent = this->eventCw;
|
e.inputEvent = this->eventCw;
|
||||||
|
inputBroker->queueInputEvent(&e);
|
||||||
break;
|
break;
|
||||||
case RotaryEncoder::DIRECTION_CCW:
|
case RotaryEncoder::DIRECTION_CCW:
|
||||||
LOG_DEBUG("Rotary event CCW");
|
LOG_DEBUG("Rotary event CCW");
|
||||||
e.inputEvent = this->eventCcw;
|
e.inputEvent = this->eventCcw;
|
||||||
|
inputBroker->queueInputEvent(&e);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (e.inputEvent != INPUT_BROKER_NONE) {
|
|
||||||
this->notifyObservers(&e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
|
// This is a version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
|
||||||
|
|
||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
@@ -8,21 +8,21 @@
|
|||||||
|
|
||||||
class RotaryEncoder;
|
class RotaryEncoder;
|
||||||
|
|
||||||
class RotaryEncoderImpl : public Observable<const InputEvent *>, public concurrency::OSThread
|
class RotaryEncoderImpl : public InputPollable
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RotaryEncoderImpl();
|
RotaryEncoderImpl();
|
||||||
bool init(void);
|
bool init(void);
|
||||||
|
virtual void pollOnce() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual int32_t runOnce() override;
|
static RotaryEncoderImpl *interruptInstance;
|
||||||
|
|
||||||
input_broker_event eventCw = INPUT_BROKER_NONE;
|
input_broker_event eventCw = INPUT_BROKER_NONE;
|
||||||
input_broker_event eventCcw = INPUT_BROKER_NONE;
|
input_broker_event eventCcw = INPUT_BROKER_NONE;
|
||||||
input_broker_event eventPressed = INPUT_BROKER_NONE;
|
input_broker_event eventPressed = INPUT_BROKER_NONE;
|
||||||
|
|
||||||
RotaryEncoder *rotary;
|
RotaryEncoder *rotary;
|
||||||
const char *originName;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern RotaryEncoderImpl *rotaryEncoderImpl;
|
extern RotaryEncoderImpl *rotaryEncoderImpl;
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu
|
|||||||
|
|
||||||
void RotaryEncoderInterruptBase::init(
|
void RotaryEncoderInterruptBase::init(
|
||||||
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
||||||
input_broker_event eventPressed,
|
input_broker_event eventPressed, input_broker_event eventPressedLong,
|
||||||
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
|
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
|
||||||
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
|
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
|
||||||
{
|
{
|
||||||
this->_pinA = pinA;
|
this->_pinA = pinA;
|
||||||
this->_pinB = pinB;
|
this->_pinB = pinB;
|
||||||
|
this->_pinPress = pinPress;
|
||||||
this->_eventCw = eventCw;
|
this->_eventCw = eventCw;
|
||||||
this->_eventCcw = eventCcw;
|
this->_eventCcw = eventCcw;
|
||||||
this->_eventPressed = eventPressed;
|
this->_eventPressed = eventPressed;
|
||||||
|
this->_eventPressedLong = eventPressedLong;
|
||||||
|
|
||||||
bool isRAK = false;
|
bool isRAK = false;
|
||||||
#ifdef RAK_4631
|
#ifdef RAK_4631
|
||||||
@@ -25,7 +27,7 @@ void RotaryEncoderInterruptBase::init(
|
|||||||
|
|
||||||
if (!isRAK || pinPress != 0) {
|
if (!isRAK || pinPress != 0) {
|
||||||
pinMode(pinPress, INPUT_PULLUP);
|
pinMode(pinPress, INPUT_PULLUP);
|
||||||
attachInterrupt(pinPress, onIntPress, RISING);
|
attachInterrupt(pinPress, onIntPress, CHANGE);
|
||||||
}
|
}
|
||||||
if (!isRAK || this->_pinA != 0) {
|
if (!isRAK || this->_pinA != 0) {
|
||||||
pinMode(this->_pinA, INPUT_PULLUP);
|
pinMode(this->_pinA, INPUT_PULLUP);
|
||||||
@@ -43,13 +45,40 @@ void RotaryEncoderInterruptBase::init(
|
|||||||
|
|
||||||
int32_t RotaryEncoderInterruptBase::runOnce()
|
int32_t RotaryEncoderInterruptBase::runOnce()
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
// Handle press long/short detection
|
||||||
if (this->action == ROTARY_ACTION_PRESSED) {
|
if (this->action == ROTARY_ACTION_PRESSED) {
|
||||||
LOG_DEBUG("Rotary event Press");
|
bool buttonPressed = !digitalRead(_pinPress);
|
||||||
|
if (!pressDetected && buttonPressed) {
|
||||||
|
pressDetected = true;
|
||||||
|
pressStartTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pressDetected) {
|
||||||
|
uint32_t duration = now - pressStartTime;
|
||||||
|
if (!buttonPressed) {
|
||||||
|
// released -> if short press, send short, else already sent long
|
||||||
|
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
|
||||||
|
lastPressKeyTime = now;
|
||||||
|
LOG_DEBUG("Rotary event Press short");
|
||||||
e.inputEvent = this->_eventPressed;
|
e.inputEvent = this->_eventPressed;
|
||||||
|
}
|
||||||
|
pressDetected = false;
|
||||||
|
pressStartTime = 0;
|
||||||
|
lastPressLongEventTime = 0;
|
||||||
|
this->action = ROTARY_ACTION_NONE;
|
||||||
|
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
|
||||||
|
lastPressLongEventTime == 0) {
|
||||||
|
// fire single-shot long press
|
||||||
|
lastPressLongEventTime = now;
|
||||||
|
LOG_DEBUG("Rotary event Press long");
|
||||||
|
e.inputEvent = this->_eventPressedLong;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this->action == ROTARY_ACTION_CW) {
|
} else if (this->action == ROTARY_ACTION_CW) {
|
||||||
LOG_DEBUG("Rotary event CW");
|
LOG_DEBUG("Rotary event CW");
|
||||||
e.inputEvent = this->_eventCw;
|
e.inputEvent = this->_eventCw;
|
||||||
@@ -62,7 +91,9 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
|||||||
this->notifyObservers(&e);
|
this->notifyObservers(&e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pressDetected) {
|
||||||
this->action = ROTARY_ACTION_NONE;
|
this->action = ROTARY_ACTION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
return INT32_MAX;
|
return INT32_MAX;
|
||||||
}
|
}
|
||||||
@@ -70,7 +101,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
|||||||
void RotaryEncoderInterruptBase::intPressHandler()
|
void RotaryEncoderInterruptBase::intPressHandler()
|
||||||
{
|
{
|
||||||
this->action = ROTARY_ACTION_PRESSED;
|
this->action = ROTARY_ACTION_PRESSED;
|
||||||
setIntervalFromNow(20); // TODO: this modifies a non-volatile variable!
|
setIntervalFromNow(20); // start checking for long/short
|
||||||
}
|
}
|
||||||
|
|
||||||
void RotaryEncoderInterruptBase::intAHandler()
|
void RotaryEncoderInterruptBase::intAHandler()
|
||||||
@@ -120,7 +151,7 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
|
|||||||
// Logic to prevent bouncing.
|
// Logic to prevent bouncing.
|
||||||
newState = ROTARY_EVENT_CLEARED;
|
newState = ROTARY_EVENT_CLEARED;
|
||||||
}
|
}
|
||||||
setIntervalFromNow(50); // TODO: this modifies a non-volatile variable!
|
setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable!
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
|||||||
public:
|
public:
|
||||||
explicit RotaryEncoderInterruptBase(const char *name);
|
explicit RotaryEncoderInterruptBase(const char *name);
|
||||||
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
|
||||||
input_broker_event eventPressed,
|
input_broker_event eventPressed, input_broker_event eventPressedLong,
|
||||||
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
|
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
|
||||||
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
|
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
|
||||||
void intPressHandler();
|
void intPressHandler();
|
||||||
@@ -33,10 +33,22 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
|
|||||||
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
|
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// pins and events
|
||||||
uint8_t _pinA = 0;
|
uint8_t _pinA = 0;
|
||||||
uint8_t _pinB = 0;
|
uint8_t _pinB = 0;
|
||||||
|
uint8_t _pinPress = 0;
|
||||||
input_broker_event _eventCw = INPUT_BROKER_NONE;
|
input_broker_event _eventCw = INPUT_BROKER_NONE;
|
||||||
input_broker_event _eventCcw = INPUT_BROKER_NONE;
|
input_broker_event _eventCcw = INPUT_BROKER_NONE;
|
||||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||||
|
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
|
||||||
const char *_originName;
|
const char *_originName;
|
||||||
|
|
||||||
|
// Long press detection variables
|
||||||
|
uint32_t pressStartTime = 0;
|
||||||
|
bool pressDetected = false;
|
||||||
|
uint32_t lastPressLongEventTime = 0;
|
||||||
|
unsigned long lastPressKeyTime = 0;
|
||||||
|
static const uint32_t LONG_PRESS_DURATION = 300; // ms
|
||||||
|
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select
|
||||||
|
const unsigned long pressDebounceMs = 200; // ms
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "RotaryEncoderInterruptImpl1.h"
|
#include "RotaryEncoderInterruptImpl1.h"
|
||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
|
extern bool osk_found;
|
||||||
|
|
||||||
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
|
||||||
|
|
||||||
@@ -19,12 +20,14 @@ bool RotaryEncoderInterruptImpl1::init()
|
|||||||
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
|
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
|
||||||
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
|
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
|
||||||
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
|
||||||
|
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
|
||||||
|
|
||||||
// moduleConfig.canned_message.ext_notification_module_output
|
// moduleConfig.canned_message.ext_notification_module_output
|
||||||
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed,
|
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
|
||||||
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
|
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
|
||||||
RotaryEncoderInterruptImpl1::handleIntPressed);
|
RotaryEncoderInterruptImpl1::handleIntPressed);
|
||||||
inputBroker->registerSource(this);
|
inputBroker->registerSource(this);
|
||||||
|
osk_found = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ bool SeesawRotary::init()
|
|||||||
|
|
||||||
int32_t SeesawRotary::runOnce()
|
int32_t SeesawRotary::runOnce()
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
bool currentlyPressed = !ss.digitalRead(SS_SWITCH);
|
bool currentlyPressed = !ss.digitalRead(SS_SWITCH);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
|
|||||||
|
|
||||||
void SerialKeyboard::erase()
|
void SerialKeyboard::erase()
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_BACK;
|
e.inputEvent = INPUT_BROKER_BACK;
|
||||||
e.kbchar = 0x08;
|
e.kbchar = 0x08;
|
||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
@@ -80,7 +80,7 @@ int32_t SerialKeyboard::runOnce()
|
|||||||
|
|
||||||
if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but
|
if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but
|
||||||
// shouldn't be a limitation
|
// shouldn't be a limitation
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
// SELECT OR SEND OR CANCEL EVENT
|
// SELECT OR SEND OR CANCEL EVENT
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ uint8_t TCA8418KeyboardBase::flush()
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TCA8418KeyboardBase::clearInt()
|
||||||
|
{
|
||||||
|
writeRegister(TCA8418_REG_INT_STAT, 3);
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const
|
uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const
|
||||||
{
|
{
|
||||||
if (pinnum > TCA8418_COL9)
|
if (pinnum > TCA8418_COL9)
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class TCA8418KeyboardBase
|
|||||||
virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR);
|
virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR);
|
||||||
|
|
||||||
virtual void reset(void);
|
virtual void reset(void);
|
||||||
|
void clearInt(void);
|
||||||
|
|
||||||
virtual void trigger(void);
|
virtual void trigger(void);
|
||||||
|
|
||||||
virtual void setBacklight(bool on);
|
virtual void setBacklight(bool on);
|
||||||
|
|||||||
@@ -105,7 +105,14 @@ void TLoraPagerKeyboard::trigger()
|
|||||||
|
|
||||||
void TLoraPagerKeyboard::setBacklight(bool on)
|
void TLoraPagerKeyboard::setBacklight(bool on)
|
||||||
{
|
{
|
||||||
toggleBacklight(!on);
|
uint32_t _brightness = 0;
|
||||||
|
if (on)
|
||||||
|
_brightness = brightness;
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||||
|
ledcWrite(KB_BL_PIN, _brightness);
|
||||||
|
#else
|
||||||
|
ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void TLoraPagerKeyboard::pressed(uint8_t key)
|
void TLoraPagerKeyboard::pressed(uint8_t key)
|
||||||
@@ -192,7 +199,6 @@ void TLoraPagerKeyboard::hapticFeedback()
|
|||||||
// toggle brightness of the backlight in three steps
|
// toggle brightness of the backlight in three steps
|
||||||
void TLoraPagerKeyboard::toggleBacklight(bool off)
|
void TLoraPagerKeyboard::toggleBacklight(bool off)
|
||||||
{
|
{
|
||||||
static uint32_t brightness = 0;
|
|
||||||
if (off) {
|
if (off) {
|
||||||
brightness = 0;
|
brightness = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -206,11 +212,7 @@ void TLoraPagerKeyboard::toggleBacklight(bool off)
|
|||||||
}
|
}
|
||||||
LOG_DEBUG("Toggle backlight: %d", brightness);
|
LOG_DEBUG("Toggle backlight: %d", brightness);
|
||||||
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
setBacklight(true);
|
||||||
ledcWrite(KB_BL_PIN, brightness);
|
|
||||||
#else
|
|
||||||
ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)
|
void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
|
|||||||
uint32_t last_tap;
|
uint32_t last_tap;
|
||||||
uint8_t char_idx;
|
uint8_t char_idx;
|
||||||
int32_t tap_interval;
|
int32_t tap_interval;
|
||||||
|
uint32_t brightness = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTo
|
|||||||
void TouchScreenImpl1::init()
|
void TouchScreenImpl1::init()
|
||||||
{
|
{
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
if (settingsMap[touchscreenModule]) {
|
if (portduino_config.touchscreenModule) {
|
||||||
TouchScreenBase::init(true);
|
TouchScreenBase::init(true);
|
||||||
inputBroker->registerSource(this);
|
inputBroker->registerSource(this);
|
||||||
} else {
|
} else {
|
||||||
@@ -47,7 +47,7 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y)
|
|||||||
*/
|
*/
|
||||||
void TouchScreenImpl1::onEvent(const TouchEvent &event)
|
void TouchScreenImpl1::onEvent(const TouchEvent &event)
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.source = event.source;
|
e.source = event.source;
|
||||||
e.kbchar = 0;
|
e.kbchar = 0;
|
||||||
e.touchX = event.x;
|
e.touchX = event.x;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#include "TrackballInterruptBase.h"
|
#include "TrackballInterruptBase.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
extern bool osk_found;
|
||||||
|
|
||||||
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
|
||||||
|
|
||||||
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
|
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
|
||||||
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
|
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
|
||||||
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(),
|
input_broker_event eventRight, input_broker_event eventPressed,
|
||||||
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
|
||||||
|
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
|
||||||
{
|
{
|
||||||
this->_pinDown = pinDown;
|
this->_pinDown = pinDown;
|
||||||
this->_pinUp = pinUp;
|
this->_pinUp = pinUp;
|
||||||
@@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
|||||||
this->_eventLeft = eventLeft;
|
this->_eventLeft = eventLeft;
|
||||||
this->_eventRight = eventRight;
|
this->_eventRight = eventRight;
|
||||||
this->_eventPressed = eventPressed;
|
this->_eventPressed = eventPressed;
|
||||||
|
this->_eventPressedLong = eventPressedLong;
|
||||||
|
|
||||||
if (pinPress != 255) {
|
if (pinPress != 255) {
|
||||||
pinMode(pinPress, INPUT_PULLUP);
|
pinMode(pinPress, INPUT_PULLUP);
|
||||||
@@ -40,20 +43,57 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
|||||||
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
|
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
|
LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
|
||||||
pinPress);
|
this->_pinLeft, this->_pinRight, pinPress);
|
||||||
|
osk_found = true;
|
||||||
this->setInterval(100);
|
this->setInterval(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t TrackballInterruptBase::runOnce()
|
int32_t TrackballInterruptBase::runOnce()
|
||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
|
||||||
if (this->action == TB_ACTION_PRESSED) {
|
// Handle long press detection for press button
|
||||||
// LOG_DEBUG("Trackball event Press");
|
if (pressDetected && pressStartTime > 0) {
|
||||||
|
uint32_t pressDuration = millis() - pressStartTime;
|
||||||
|
bool buttonStillPressed = false;
|
||||||
|
|
||||||
|
#if defined(T_DECK)
|
||||||
|
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
|
||||||
|
#else
|
||||||
|
buttonStillPressed = !digitalRead(_pinPress);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!buttonStillPressed) {
|
||||||
|
// Button released
|
||||||
|
if (pressDuration < LONG_PRESS_DURATION) {
|
||||||
|
// Short press
|
||||||
e.inputEvent = this->_eventPressed;
|
e.inputEvent = this->_eventPressed;
|
||||||
|
}
|
||||||
|
// Reset state
|
||||||
|
pressDetected = false;
|
||||||
|
pressStartTime = 0;
|
||||||
|
lastLongPressEventTime = 0;
|
||||||
|
this->action = TB_ACTION_NONE;
|
||||||
|
} else if (pressDuration >= LONG_PRESS_DURATION) {
|
||||||
|
// Long press detected
|
||||||
|
uint32_t currentTime = millis();
|
||||||
|
// Only trigger long press event if enough time has passed since the last one
|
||||||
|
if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
|
||||||
|
e.inputEvent = this->_eventPressedLong;
|
||||||
|
lastLongPressEventTime = currentTime;
|
||||||
|
}
|
||||||
|
this->action = TB_ACTION_PRESSED_LONG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
|
||||||
|
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
|
||||||
|
// Start long press detection
|
||||||
|
pressDetected = true;
|
||||||
|
pressStartTime = millis();
|
||||||
|
// Don't send event yet, wait to see if it's a long press
|
||||||
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
|
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
|
||||||
// LOG_DEBUG("Trackball event UP");
|
// LOG_DEBUG("Trackball event UP");
|
||||||
e.inputEvent = this->_eventUp;
|
e.inputEvent = this->_eventUp;
|
||||||
@@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
e.inputEvent = this->_eventRight;
|
e.inputEvent = this->_eventRight;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
|
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
|
||||||
// LOG_DEBUG("Trackball event Press");
|
// Start long press detection
|
||||||
e.inputEvent = this->_eventPressed;
|
pressDetected = true;
|
||||||
|
pressStartTime = millis();
|
||||||
|
// Don't send event yet, wait to see if it's a long press
|
||||||
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
|
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
|
||||||
// LOG_DEBUG("Trackball event UP");
|
// LOG_DEBUG("Trackball event UP");
|
||||||
e.inputEvent = this->_eventUp;
|
e.inputEvent = this->_eventUp;
|
||||||
@@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
e.kbchar = 0x00;
|
e.kbchar = 0x00;
|
||||||
this->notifyObservers(&e);
|
this->notifyObservers(&e);
|
||||||
}
|
}
|
||||||
lastEvent = action;
|
|
||||||
this->action = TB_ACTION_NONE;
|
|
||||||
|
|
||||||
return 100;
|
// Only update lastEvent for non-press actions or completed press actions
|
||||||
|
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
|
||||||
|
lastEvent = action;
|
||||||
|
if (!pressDetected) {
|
||||||
|
this->action = TB_ACTION_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 50; // Check more frequently for better long press detection
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackballInterruptBase::intPressHandler()
|
void TrackballInterruptBase::intPressHandler()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user