mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-03-31 01:45:45 +00:00
Compare commits
524 Commits
room-serve
...
repeater-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b92d9bd972 | ||
|
|
3335b49d9f | ||
|
|
e5de6e6600 | ||
|
|
cd7e7d9bbe | ||
|
|
4bb16ef5a7 | ||
|
|
70ec996c08 | ||
|
|
3f4f9eff17 | ||
|
|
0767fc49e5 | ||
|
|
c83abbeff6 | ||
|
|
030f0d5d82 | ||
|
|
0307b6119e | ||
|
|
2e92137d10 | ||
|
|
58ed14d971 | ||
|
|
f8f5f00549 | ||
|
|
f9b2613e57 | ||
|
|
f3b9c06646 | ||
|
|
2992062bbe | ||
|
|
0beaa323ed | ||
|
|
cc822c029b | ||
|
|
95e533d60b | ||
|
|
e49eef5588 | ||
|
|
3fbdaf7ce6 | ||
|
|
7bcf1f1b47 | ||
|
|
84feb63ed5 | ||
|
|
a3e6b79c2f | ||
|
|
74e1b6c75b | ||
|
|
418ae08b4d | ||
|
|
b8394a4e62 | ||
|
|
1c7a0ce2bd | ||
|
|
02c178dae7 | ||
|
|
a5af1b5bcd | ||
|
|
e988531f6a | ||
|
|
76be66313e | ||
|
|
c21596341a | ||
|
|
3bc8ec2006 | ||
|
|
088b8fd98c | ||
|
|
128119fe40 | ||
|
|
f2cff53b0e | ||
|
|
20b0fd1dc9 | ||
|
|
f85db18242 | ||
|
|
955b7321e8 | ||
|
|
e2fa70d6f3 | ||
|
|
b11f08422e | ||
|
|
db40a9cea6 | ||
|
|
c1915a1133 | ||
|
|
ea13fa899e | ||
|
|
4aa58ade8a | ||
|
|
3885d47ec9 | ||
|
|
adecd1e58d | ||
|
|
611d61b6c6 | ||
|
|
f100894882 | ||
|
|
4579a1bcaf | ||
|
|
669bea04a0 | ||
|
|
881396eeaf | ||
|
|
0cb34740d2 | ||
|
|
c9b060aefb | ||
|
|
d85d364431 | ||
|
|
52d5cc6068 | ||
|
|
28d673ee15 | ||
|
|
f9543bb7bb | ||
|
|
59ea6cdb89 | ||
|
|
695473f842 | ||
|
|
4daad75f7d | ||
|
|
2922b62888 | ||
|
|
757ff9fb55 | ||
|
|
a1622bad75 | ||
|
|
b3af4d9c72 | ||
|
|
736118fe6b | ||
|
|
b464f5c640 | ||
|
|
985b290d02 | ||
|
|
384b02bec4 | ||
|
|
b3e9fd76ce | ||
|
|
f77fd15707 | ||
|
|
e35e4bb23e | ||
|
|
8ddabfcffa | ||
|
|
9ba8d6f23f | ||
|
|
6f8ce425d8 | ||
|
|
043f37a08e | ||
|
|
2da50882c0 | ||
|
|
bd6aa983a3 | ||
|
|
fca16f1b71 | ||
|
|
47c57a52cc | ||
|
|
19fb7aae63 | ||
|
|
d86851b881 | ||
|
|
98b524bfcf | ||
|
|
a288ac06a6 | ||
|
|
88786a906f | ||
|
|
845a497604 | ||
|
|
81180bbf8c | ||
|
|
f9428b7d27 | ||
|
|
fa3e4f9715 | ||
|
|
d377ffd393 | ||
|
|
400e09f318 | ||
|
|
561dbea30f | ||
|
|
ded81780a4 | ||
|
|
21ea63bcd9 | ||
|
|
5ccacb2a91 | ||
|
|
ce08db6238 | ||
|
|
5377d7cc17 | ||
|
|
3ef2aa6a95 | ||
|
|
9b2dbf51cb | ||
|
|
a6a0183d44 | ||
|
|
de2e0cf59c | ||
|
|
c69d78b62e | ||
|
|
9df6e8a6b6 | ||
|
|
5cd0470879 | ||
|
|
b5820b1ce0 | ||
|
|
25ea953cc3 | ||
|
|
281591f147 | ||
|
|
d929d32569 | ||
|
|
510472bfa0 | ||
|
|
e42ecc3bb3 | ||
|
|
95d1f052c2 | ||
|
|
ce39df599c | ||
|
|
3b82224db6 | ||
|
|
c8a10cc3b3 | ||
|
|
1257c6b181 | ||
|
|
65ef6c2fd0 | ||
|
|
f35e259fd6 | ||
|
|
80d5e2d8bc | ||
|
|
d83cdc501f | ||
|
|
2d4b77c998 | ||
|
|
cf93109cd5 | ||
|
|
3666cd72e5 | ||
|
|
e35183ae41 | ||
|
|
5344f04d89 | ||
|
|
08f91f8d95 | ||
|
|
18d6d54c07 | ||
|
|
f92bd0db9e | ||
|
|
e8314c9c8c | ||
|
|
ea33f39557 | ||
|
|
ecd2e12894 | ||
|
|
bb29b66b29 | ||
|
|
0dfd2bcbb8 | ||
|
|
a55fa8d8ec | ||
|
|
1c93c162a1 | ||
|
|
1d25c87c57 | ||
|
|
c44d84ca9b | ||
|
|
adaad00b19 | ||
|
|
a0e7b47e29 | ||
|
|
f2e8fb0259 | ||
|
|
a44b8e626a | ||
|
|
74dea260e5 | ||
|
|
6a9dedf0b4 | ||
|
|
7fca20475a | ||
|
|
0051ccef26 | ||
|
|
537449e6af | ||
|
|
04e70829a4 | ||
|
|
5b9d11ac8f | ||
|
|
006605ce1d | ||
|
|
73b49ea14d | ||
|
|
5370667bd8 | ||
|
|
7363a4f67d | ||
|
|
f6f0cfd603 | ||
|
|
b0c7ea45c0 | ||
|
|
0088509df4 | ||
|
|
ea4ed2abec | ||
|
|
6da6504b80 | ||
|
|
18be92615b | ||
|
|
acf6110001 | ||
|
|
8521b0eb08 | ||
|
|
c10c010736 | ||
|
|
ac8ec172ef | ||
|
|
132ca72735 | ||
|
|
84623938c3 | ||
|
|
1c0154279a | ||
|
|
605210dd07 | ||
|
|
5b8c8b0bf6 | ||
|
|
bcfc8d3771 | ||
|
|
3d83556829 | ||
|
|
accd1e0a97 | ||
|
|
2b24c575c7 | ||
|
|
bdfe9ad27b | ||
|
|
c5180d4588 | ||
|
|
2ef38422e9 | ||
|
|
808214d7b5 | ||
|
|
d59724acd0 | ||
|
|
0ebca4b88e | ||
|
|
ec332c442b | ||
|
|
cb99eb4ae8 | ||
|
|
8fdaaceb1c | ||
|
|
f974cb2a4f | ||
|
|
2d651221c4 | ||
|
|
5843a12c71 | ||
|
|
6fae950814 | ||
|
|
8f3c0a3ad2 | ||
|
|
24b2953861 | ||
|
|
8549696e4d | ||
|
|
c9e6ae9e6c | ||
|
|
2aa6835064 | ||
|
|
963556f9ba | ||
|
|
375093f78d | ||
|
|
0e3933f18a | ||
|
|
c396ed9a05 | ||
|
|
77ab19153e | ||
|
|
2b920dfed3 | ||
|
|
ee3c4baea5 | ||
|
|
1948d284a0 | ||
|
|
9b9c7289e6 | ||
|
|
816bbf925f | ||
|
|
5b2c1715f4 | ||
|
|
d8f80f259a | ||
|
|
1f20722f51 | ||
|
|
f9079985b6 | ||
|
|
46b3910d81 | ||
|
|
a3aa66ac16 | ||
|
|
d56b725256 | ||
|
|
8fa31e00aa | ||
|
|
f4df94a20e | ||
|
|
6e6c59d2ce | ||
|
|
a9fef1aefa | ||
|
|
13d046892a | ||
|
|
5782c2e799 | ||
|
|
3e7459ae2e | ||
|
|
6334971e2b | ||
|
|
c2fc70047a | ||
|
|
72b267092f | ||
|
|
cbf3a03d2e | ||
|
|
d610b7be86 | ||
|
|
1c91298b3a | ||
|
|
9f97edcb21 | ||
|
|
cb3049e706 | ||
|
|
96a71bb21b | ||
|
|
afbfc6c6ed | ||
|
|
a9ab1f072a | ||
|
|
9f185303b4 | ||
|
|
5de0dc1fd6 | ||
|
|
43c3105bf1 | ||
|
|
ce31fd7c57 | ||
|
|
ddc900c8c8 | ||
|
|
a93a0fecba | ||
|
|
03358b33c2 | ||
|
|
90cb1e73f9 | ||
|
|
3cdf2f9b4d | ||
|
|
c9671d7d8d | ||
|
|
88fbb41016 | ||
|
|
1a41da6bf2 | ||
|
|
2546a5da07 | ||
|
|
b863a1a673 | ||
|
|
b64e78b7eb | ||
|
|
c3fb3bcefe | ||
|
|
4849b863e9 | ||
|
|
f3c52d84db | ||
|
|
accacd9d74 | ||
|
|
9fd7e9427a | ||
|
|
cf4720bd34 | ||
|
|
1130cf13ab | ||
|
|
637891b814 | ||
|
|
a4c2da9d50 | ||
|
|
3ad43431d9 | ||
|
|
74722c24b8 | ||
|
|
b8223e9d07 | ||
|
|
81afd83099 | ||
|
|
ee194a7b19 | ||
|
|
c28001d1e2 | ||
|
|
7bc02296ff | ||
|
|
76711f54ce | ||
|
|
fae3c284d3 | ||
|
|
3aa57780f1 | ||
|
|
489bcaffc9 | ||
|
|
4413e5be95 | ||
|
|
8b3c16c497 | ||
|
|
7c7faaab05 | ||
|
|
009ca6d6aa | ||
|
|
e1ac794a81 | ||
|
|
f5c7d3dd80 | ||
|
|
7a00f3060e | ||
|
|
50cab44473 | ||
|
|
8a39e80359 | ||
|
|
615316f443 | ||
|
|
e8d4ab5977 | ||
|
|
7854244026 | ||
|
|
0f23c0120a | ||
|
|
d8c4fa456a | ||
|
|
b3adaa790a | ||
|
|
18ef1ba804 | ||
|
|
6172537459 | ||
|
|
5a34bd5460 | ||
|
|
d4856a5275 | ||
|
|
e1c169b20a | ||
|
|
fee7975668 | ||
|
|
31006857fe | ||
|
|
bc8c95fcb3 | ||
|
|
c5b28da41d | ||
|
|
4eaaeebfca | ||
|
|
3e3e364dae | ||
|
|
4785240608 | ||
|
|
796b4c705c | ||
|
|
4d6e33b365 | ||
|
|
980c3445d0 | ||
|
|
a0b037ecd7 | ||
|
|
6984d9f496 | ||
|
|
09e45f25b7 | ||
|
|
d012dc7fd7 | ||
|
|
9ee0152084 | ||
|
|
c49ecc121e | ||
|
|
19978d6b6e | ||
|
|
f9f1c2e340 | ||
|
|
13cd849fcd | ||
|
|
789629f5f8 | ||
|
|
3fe2d48a61 | ||
|
|
d93e03bb6e | ||
|
|
ec6d119900 | ||
|
|
bb63f8165d | ||
|
|
2e49eef337 | ||
|
|
d5bec3d6a5 | ||
|
|
1415792141 | ||
|
|
666b036d3d | ||
|
|
3f5c772663 | ||
|
|
2d6e714ccd | ||
|
|
136f733df5 | ||
|
|
c10b387e63 | ||
|
|
6536e9931d | ||
|
|
0959e64d11 | ||
|
|
a16e011bd2 | ||
|
|
24cc6a40a6 | ||
|
|
0ed8921153 | ||
|
|
4a166078db | ||
|
|
02ad2bed4d | ||
|
|
78fcb704bc | ||
|
|
df18dfb481 | ||
|
|
15249bb8d5 | ||
|
|
033706adcf | ||
|
|
2d5016bac3 | ||
|
|
d0fb8d2f30 | ||
|
|
08b49c3ac5 | ||
|
|
0fd24c8e5d | ||
|
|
44fb4d1bce | ||
|
|
bbee057b03 | ||
|
|
b88a360ada | ||
|
|
228bac0add | ||
|
|
685f75234b | ||
|
|
dedef49315 | ||
|
|
cb96503b92 | ||
|
|
7f142245e6 | ||
|
|
5cd1df48ad | ||
|
|
0c37eafd01 | ||
|
|
e0d548c71e | ||
|
|
c636536599 | ||
|
|
82184c5836 | ||
|
|
5772756b30 | ||
|
|
f462113f4c | ||
|
|
412e9d4678 | ||
|
|
5dc930410c | ||
|
|
a9d4cf1d21 | ||
|
|
e846cc6798 | ||
|
|
ff03b041d0 | ||
|
|
a825a3d4bc | ||
|
|
d145d5936d | ||
|
|
b8c4c75025 | ||
|
|
b8f80afee9 | ||
|
|
6e296e8db1 | ||
|
|
cb85600572 | ||
|
|
a4916f81eb | ||
|
|
cb8ca91d27 | ||
|
|
3bb55b590c | ||
|
|
8f1ccb65ae | ||
|
|
e6152f9d6c | ||
|
|
ed3f52775a | ||
|
|
7613b9455d | ||
|
|
76a53bf84d | ||
|
|
b332b06304 | ||
|
|
fe376e8c35 | ||
|
|
f5ad1df103 | ||
|
|
276a057693 | ||
|
|
2477d60fae | ||
|
|
8f8830047b | ||
|
|
c30a103baf | ||
|
|
95c9d17dc5 | ||
|
|
022bfc4f4b | ||
|
|
0359df6cb5 | ||
|
|
f9284cdf4a | ||
|
|
37d7257f04 | ||
|
|
e14b022a7c | ||
|
|
acde9921b5 | ||
|
|
29fd5da5e8 | ||
|
|
e9ffc3ea93 | ||
|
|
86671c0ff8 | ||
|
|
bd6bd065ac | ||
|
|
a5ebac6236 | ||
|
|
9108a709ee | ||
|
|
655d4a78f5 | ||
|
|
24ef375fc7 | ||
|
|
d7c2293cb8 | ||
|
|
a7dcd112ac | ||
|
|
aa7f9d8df6 | ||
|
|
82206fd281 | ||
|
|
e47a1df67f | ||
|
|
6d18e2c57b | ||
|
|
fad4a7fb51 | ||
|
|
dc9b4f8e84 | ||
|
|
be243a2663 | ||
|
|
dc6b830970 | ||
|
|
86ec82fd06 | ||
|
|
e84e3066ff | ||
|
|
1897f51458 | ||
|
|
a2eed714f5 | ||
|
|
bed311313a | ||
|
|
af7db5593b | ||
|
|
c8bbec6549 | ||
|
|
153051ab82 | ||
|
|
67529d0cf3 | ||
|
|
72d13ca867 | ||
|
|
b9270aff5c | ||
|
|
cdc762ada2 | ||
|
|
6e26a6a78c | ||
|
|
cc065c84ba | ||
|
|
331a29b082 | ||
|
|
6902dd81fa | ||
|
|
af72db6834 | ||
|
|
1e711f57f4 | ||
|
|
1b93ceaa30 | ||
|
|
8d7a49867f | ||
|
|
4b95c981bb | ||
|
|
ce4e559c01 | ||
|
|
a310a5c4d5 | ||
|
|
4d97bee02a | ||
|
|
6b2836ea07 | ||
|
|
53c1f70412 | ||
|
|
4b653408a6 | ||
|
|
fa3500944b | ||
|
|
d1e13d0b9e | ||
|
|
6214b75e83 | ||
|
|
28360ba459 | ||
|
|
ca9687e212 | ||
|
|
fc334a05c6 | ||
|
|
00dc193b0d | ||
|
|
cf9bcb5f4f | ||
|
|
fb8a4d12b1 | ||
|
|
612dde73e9 | ||
|
|
2853708f38 | ||
|
|
112e60a14a | ||
|
|
6861b0702f | ||
|
|
a5c78f2f19 | ||
|
|
8d3bdc6945 | ||
|
|
d13dc10bf3 | ||
|
|
a49b5aaba7 | ||
|
|
febc63d286 | ||
|
|
b17196828d | ||
|
|
28af68c187 | ||
|
|
0a2d132d84 | ||
|
|
2824fc31a4 | ||
|
|
32e8ce4130 | ||
|
|
fcdf342db6 | ||
|
|
1dfc0e6975 | ||
|
|
ea2ce93c02 | ||
|
|
f87e856347 | ||
|
|
f66d900ae2 | ||
|
|
84eafe4752 | ||
|
|
dca20ea994 | ||
|
|
85273a6dc6 | ||
|
|
131e7a5a23 | ||
|
|
822850b4d5 | ||
|
|
a96d1022a0 | ||
|
|
5f9210b5db | ||
|
|
ef58ef460b | ||
|
|
b0946b3f6b | ||
|
|
16820c5289 | ||
|
|
39eb5502af | ||
|
|
55453e1136 | ||
|
|
12f5177229 | ||
|
|
ad19ac1ab3 | ||
|
|
2e346bc61c | ||
|
|
c1041af5a1 | ||
|
|
365cb89634 | ||
|
|
048fa03784 | ||
|
|
3139d509c2 | ||
|
|
4689f9b425 | ||
|
|
ea4aa93594 | ||
|
|
9485488f6e | ||
|
|
e48e64ae84 | ||
|
|
479b8ed0ce | ||
|
|
c2266026a0 | ||
|
|
b5a8a1a883 | ||
|
|
e42d8f972e | ||
|
|
f88ebad604 | ||
|
|
296a1e45fb | ||
|
|
0a9da09a67 | ||
|
|
599e3a187c | ||
|
|
c6d4b7513f | ||
|
|
c5783660c4 | ||
|
|
a2e3e6607e | ||
|
|
64cc4cf60a | ||
|
|
9be28c2002 | ||
|
|
93802fe250 | ||
|
|
9f2a77c92e | ||
|
|
e4f7b9e37f | ||
|
|
855e4831f5 | ||
|
|
e9a8fcb1cd | ||
|
|
6b4592bfe2 | ||
|
|
73b1ac5190 | ||
|
|
46d30f6bfe | ||
|
|
660ab0692f | ||
|
|
2c9dc8d351 | ||
|
|
6a6221f44e | ||
|
|
46fa3f2026 | ||
|
|
122f5fa10a | ||
|
|
58cffa8f76 | ||
|
|
3358783039 | ||
|
|
5881b04a31 | ||
|
|
6bc8dd28d4 | ||
|
|
3a0dfc1bf3 | ||
|
|
d15b374c29 | ||
|
|
3f996ef4fc | ||
|
|
57f93a4196 | ||
|
|
a6c8dc4866 | ||
|
|
c26418016b | ||
|
|
3adbb5042e | ||
|
|
4fcbc00bea | ||
|
|
6be8e19a9f | ||
|
|
d84feacc60 | ||
|
|
04042e3ca0 | ||
|
|
1f23632751 | ||
|
|
97b51900f8 | ||
|
|
92ee1820c4 | ||
|
|
ac056fb0b9 | ||
|
|
7bec45b3dd | ||
|
|
3375389181 | ||
|
|
2f77cef04b | ||
|
|
f7920114c5 | ||
|
|
4b70ee863d | ||
|
|
127f3a7640 | ||
|
|
70252b010c | ||
|
|
b1ca3d1eb1 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,3 +9,8 @@ out/
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
.vscode/extensions.json
|
||||
.idea
|
||||
cmake-*
|
||||
.cache
|
||||
.ccls
|
||||
compile_commands.json
|
||||
|
||||
20
README.md
20
README.md
@@ -9,7 +9,10 @@ MeshCore provides the ability to create wireless mesh networks, similar to Mesht
|
||||
|
||||
## ⚡ Key Features
|
||||
|
||||
* Multi-Hop Packet Routing – Devices can forward messages across multiple nodes, extending range beyond a single radio's reach. MeshCore supports up to a configurable number of hops to balance network efficiency and prevent excessive traffic.
|
||||
* Multi-Hop Packet Routing
|
||||
* Devices can forward messages across multiple nodes, extending range beyond a single radio's reach.
|
||||
* Supports up to a configurable number of hops to balance network efficiency and prevent excessive traffic.
|
||||
* Nodes use fixed roles where "Companion" nodes are not repeating messages at all to prevent adverse routing paths from being used.
|
||||
* Supports LoRa Radios – Works with Heltec, RAK Wireless, and other LoRa-based hardware.
|
||||
* Decentralized & Resilient – No central server or internet required; the network is self-healing.
|
||||
* Low Power Consumption – Ideal for battery-powered or solar-powered devices.
|
||||
@@ -90,6 +93,21 @@ Here are some general principals you should try to adhere to:
|
||||
* No dynamic memory allocation, except during setup/begin functions.
|
||||
* Use the same brace and indenting style that's in the core source modules. (A .clang-format is prob going to be added soon, but please do NOT retroactively re-format existing code. This just creates unnecessary diffs that make finding problems harder)
|
||||
|
||||
## Road-Map / To-Do
|
||||
|
||||
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:
|
||||
- [X] Companion radio: UI redesign
|
||||
- [ ] Repeater + Room Server: add ACL's (like Sensor Node has)
|
||||
- [ ] Standardise Bridge mode for repeaters
|
||||
- [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering
|
||||
- [ ] Core + Repeater: enhanced zero-hop neighbour discovery
|
||||
- [ ] Core: round-trip manual path support
|
||||
- [ ] Companion + Apps: support for multiple sub-meshes (and 'off-grid' client repeat mode)
|
||||
- [ ] Core + Apps: support for LZW message compression
|
||||
- [ ] Core: dynamic CR (Coding Rate) for weak vs strong hops
|
||||
- [ ] Core: new framework for hosting multiple virtual nodes on one physical device
|
||||
- [ ] V2 protocol spec: discussion and concensus around V2 packet protocol, including path hashes, new encryption specs, etc
|
||||
|
||||
## 📞 Get Support
|
||||
|
||||
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
|
||||
|
||||
45
boards/ebyte_eora-s3.json
Normal file
45
boards/ebyte_eora-s3.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default.csv",
|
||||
"memory_type": "qio_qspi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_LILYGO_T3_S3_V1_X",
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DARDUINO_USB_MODE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi"
|
||||
],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Ebyte EoRa-S3-XXXTB Radio",
|
||||
"upload": {
|
||||
"flash_size": "4MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 4194304,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.cdebyte.com/products/EoRa-S3-900TB",
|
||||
"vendor": "Chengdu Ebyte Electronic Technology Co., Ltd"
|
||||
}
|
||||
44
boards/heltec_e213.json
Normal file
44
boards/heltec_e213.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [
|
||||
["0x303A", "0x1001"],
|
||||
["0x303A", "0x0002"]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "heltec_vision_master_e213"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Heltec Vision Master E213",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://heltec.org/project/vision-master-e213/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
44
boards/heltec_e290.json
Normal file
44
boards/heltec_e290.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [
|
||||
["0x303A", "0x1001"],
|
||||
["0x303A", "0x0002"]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "heltec_vision_master_e290"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Heltec Vision Master E290",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://heltec.org/project/vision-master-e290/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
53
boards/heltec_mesh_pocket.json
Normal file
53
boards/heltec_mesh_pocket.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"]
|
||||
],
|
||||
"usb_product": "HT-n5262",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_pocket",
|
||||
"variants_dir": "variants",
|
||||
"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",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Heltec nrf (Adafruit BSP)",
|
||||
"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://heltec.org/project/meshpocket/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
61
boards/heltec_mesh_solar.json
Normal file
61
boards/heltec_mesh_solar.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"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": "HT-n5262",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "heltec_mesh_solar",
|
||||
"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": "nrf52.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "Heltec Mesh Solar 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://heltec.org/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
44
boards/heltec_t190.json
Normal file
44
boards/heltec_t190.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"hwids": [
|
||||
["0x303A", "0x1001"],
|
||||
["0x303A", "0x0002"]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "heltec_vision_master_t190"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Heltec Vision Master T190",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://heltec.org/project/vision-master-t190/",
|
||||
"vendor": "Heltec"
|
||||
}
|
||||
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"
|
||||
}
|
||||
38
boards/nrf52840_s140_v6_extrafs.ld
Normal file
38
boards/nrf52840_s140_v6_extrafs.ld
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Linker script to configure memory regions. */
|
||||
|
||||
SEARCH_DIR(.)
|
||||
GROUP(-lgcc -lc -lnosys)
|
||||
|
||||
MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000
|
||||
|
||||
/* SRAM required by Softdevice depend on
|
||||
* - Attribute Table Size (Number of Services and Characteristics)
|
||||
* - Vendor UUID count
|
||||
* - Max ATT MTU
|
||||
* - Concurrent connection peripheral + central + secure links
|
||||
* - Event Len, HVN queue, Write CMD queue
|
||||
*/
|
||||
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = ALIGN(4);
|
||||
.svc_data :
|
||||
{
|
||||
PROVIDE(__start_svc_data = .);
|
||||
KEEP(*(.svc_data))
|
||||
PROVIDE(__stop_svc_data = .);
|
||||
} > RAM
|
||||
|
||||
.fs_data :
|
||||
{
|
||||
PROVIDE(__start_fs_data = .);
|
||||
KEEP(*(.fs_data))
|
||||
PROVIDE(__stop_fs_data = .);
|
||||
} > RAM
|
||||
} INSERT AFTER .data;
|
||||
|
||||
INCLUDE "nrf52_common.ld"
|
||||
38
boards/nrf52840_s140_v7_extrafs.ld
Normal file
38
boards/nrf52840_s140_v7_extrafs.ld
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Linker script to configure memory regions. */
|
||||
|
||||
SEARCH_DIR(.)
|
||||
GROUP(-lgcc -lc -lnosys)
|
||||
|
||||
MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000
|
||||
|
||||
/* SRAM required by Softdevice depend on
|
||||
* - Attribute Table Size (Number of Services and Characteristics)
|
||||
* - Vendor UUID count
|
||||
* - Max ATT MTU
|
||||
* - Concurrent connection peripheral + central + secure links
|
||||
* - Event Len, HVN queue, Write CMD queue
|
||||
*/
|
||||
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = ALIGN(4);
|
||||
.svc_data :
|
||||
{
|
||||
PROVIDE(__start_svc_data = .);
|
||||
KEEP(*(.svc_data))
|
||||
PROVIDE(__stop_svc_data = .);
|
||||
} > RAM
|
||||
|
||||
.fs_data :
|
||||
{
|
||||
PROVIDE(__start_fs_data = .);
|
||||
KEEP(*(.fs_data))
|
||||
PROVIDE(__stop_fs_data = .);
|
||||
} > RAM
|
||||
} INSERT AFTER .data;
|
||||
|
||||
INCLUDE "nrf52_common.ld"
|
||||
@@ -40,12 +40,13 @@
|
||||
],
|
||||
"name": "Seeed Wio Tracker L1",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"maximum_ram_size": 237568,
|
||||
"maximum_size": 811008,
|
||||
"protocol": "nrfutil",
|
||||
"speed": 115200,
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"stlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"cmsis-dap",
|
||||
|
||||
38
boards/t-deck.json
Normal file
38
boards/t-deck.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "LilyGo T-Deck (16M Flash 8M PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.lilygo.cc",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
33
boards/tiny_relay.json
Normal file
33
boards/tiny_relay.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"variant_h": "variant_RAK3172_MODULE.h"
|
||||
},
|
||||
"core": "stm32",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx",
|
||||
"framework_extra_flags": {
|
||||
"arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE"
|
||||
},
|
||||
"f_cpu": "48000000L",
|
||||
"mcu": "stm32wle5ccu",
|
||||
"product_line": "STM32WLE5xx",
|
||||
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U"
|
||||
},
|
||||
"debug": {
|
||||
"default_tools": ["stlink"],
|
||||
"jlink_device": "STM32WLE5CC",
|
||||
"openocd_target": "stm32wlx",
|
||||
"svd_path": "STM32WLE5_CM4.svd"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "BB-STM32WL",
|
||||
"upload": {
|
||||
"maximum_ram_size": 65536,
|
||||
"maximum_size": 262144,
|
||||
"protocol": "stlink",
|
||||
"protocols": ["stlink", "jlink"]
|
||||
},
|
||||
"url": "https://www.st.com/en/microcontrollers-microprocessors/stm32wle5cc.html",
|
||||
"vendor": "YAOYAO"
|
||||
}
|
||||
49
build.sh
49
build.sh
@@ -3,6 +3,7 @@
|
||||
# usage
|
||||
# sh build.sh build-firmware RAK_4631_Repeater
|
||||
# sh build.sh build-firmwares
|
||||
# sh build.sh build-matching-firmwares RAK_4631
|
||||
# sh build.sh build-companion-firmwares
|
||||
# sh build.sh build-repeater-firmwares
|
||||
# sh build.sh build-room-server-firmwares
|
||||
@@ -23,6 +24,17 @@ get_pio_envs_containing_string() {
|
||||
done
|
||||
}
|
||||
|
||||
# $1 should be the string to find (case insensitive)
|
||||
get_pio_envs_ending_with_string() {
|
||||
shopt -s nocasematch
|
||||
envs=($(get_pio_envs))
|
||||
for env in "${envs[@]}"; do
|
||||
if [[ "$env" == *${1} ]]; then
|
||||
echo $env
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# build firmware for the provided pio env in $1
|
||||
build_firmware() {
|
||||
|
||||
@@ -46,8 +58,8 @@ build_firmware() {
|
||||
# e.g: RAK_4631_Repeater-v1.0.0-SHA
|
||||
FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}"
|
||||
|
||||
# export build flags for pio so we can inject firmware version info
|
||||
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
|
||||
# add firmware version info to end of existing platformio build flags in environment vars
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
|
||||
|
||||
# build firmware target
|
||||
pio run -e $1
|
||||
@@ -59,7 +71,7 @@ build_firmware() {
|
||||
|
||||
# build .uf2 for nrf52 boards
|
||||
if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then
|
||||
python bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
|
||||
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
|
||||
fi
|
||||
|
||||
# copy .bin, .uf2, and .zip to out folder
|
||||
@@ -84,6 +96,14 @@ build_all_firmwares_matching() {
|
||||
done
|
||||
}
|
||||
|
||||
# firmwares ending with $1 will be built
|
||||
build_all_firmwares_by_suffix() {
|
||||
envs=($(get_pio_envs_ending_with_string "$1"))
|
||||
for env in "${envs[@]}"; do
|
||||
build_firmware $env
|
||||
done
|
||||
}
|
||||
|
||||
build_repeater_firmwares() {
|
||||
|
||||
# # build specific repeater firmwares
|
||||
@@ -95,7 +115,7 @@ build_repeater_firmwares() {
|
||||
# build_firmware "RAK_4631_Repeater"
|
||||
|
||||
# build all repeater firmwares
|
||||
build_all_firmwares_matching "repeater"
|
||||
build_all_firmwares_by_suffix "_repeater"
|
||||
|
||||
}
|
||||
|
||||
@@ -114,8 +134,8 @@ build_companion_firmwares() {
|
||||
# build_firmware "t1000e_companion_radio_ble"
|
||||
|
||||
# build all companion firmwares
|
||||
build_all_firmwares_matching "companion_radio_usb"
|
||||
build_all_firmwares_matching "companion_radio_ble"
|
||||
build_all_firmwares_by_suffix "_companion_radio_usb"
|
||||
build_all_firmwares_by_suffix "_companion_radio_ble"
|
||||
|
||||
}
|
||||
|
||||
@@ -126,7 +146,7 @@ build_room_server_firmwares() {
|
||||
# build_firmware "RAK_4631_room_server"
|
||||
|
||||
# build all room server firmwares
|
||||
build_all_firmwares_matching "room_server"
|
||||
build_all_firmwares_by_suffix "_room_server"
|
||||
|
||||
}
|
||||
|
||||
@@ -142,8 +162,21 @@ mkdir -p out
|
||||
|
||||
# handle script args
|
||||
if [[ $1 == "build-firmware" ]]; then
|
||||
TARGETS=${@:2}
|
||||
if [ "$TARGETS" ]; then
|
||||
for env in $TARGETS; do
|
||||
build_firmware $env
|
||||
done
|
||||
else
|
||||
echo "usage: $0 build-firmware <target>"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ $1 == "build-matching-firmwares" ]]; then
|
||||
if [ "$2" ]; then
|
||||
build_firmware $2
|
||||
build_all_firmwares_matching $2
|
||||
else
|
||||
echo "usage: $0 build-matching-firmwares <build-match-spec>"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ $1 == "build-firmwares" ]]; then
|
||||
build_firmwares
|
||||
|
||||
64
build_as_lib.py
Normal file
64
build_as_lib.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from os.path import realpath
|
||||
|
||||
Import("env") # type: ignore
|
||||
menv=env # type: ignore
|
||||
|
||||
src_filter = [
|
||||
'+<*.cpp>',
|
||||
'+<helpers/*.cpp>',
|
||||
'+<helpers/sensors>',
|
||||
'+<helpers/radiolib/*.cpp>',
|
||||
'+<helpers/ui/MomentaryButton.cpp>',
|
||||
'+<helpers/ui/buzzer.cpp>',
|
||||
]
|
||||
|
||||
# add build and include dirs according to CPPDEFINES
|
||||
for item in menv.get("CPPDEFINES", []):
|
||||
|
||||
# PLATFORM HANDLING
|
||||
if item == "STM32_PLATFORM":
|
||||
src_filter.append("+<helpers/stm32/*>")
|
||||
elif item == "ESP32":
|
||||
src_filter.append("+<helpers/esp32/*>")
|
||||
elif item == "NRF52_PLATFORM":
|
||||
src_filter.append("+<helpers/nrf52/*>")
|
||||
elif item == "RP2040_PLATFORM":
|
||||
src_filter.append("+<helpers/rp2040/*>")
|
||||
|
||||
# DISPLAY HANDLING
|
||||
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
|
||||
display_class = item[1]
|
||||
src_filter.append(f"+<helpers/ui/{display_class}.cpp>")
|
||||
if (display_class == "ST7789Display") :
|
||||
src_filter.append(f"+<helpers/ui/OLEDDisplay.cpp>")
|
||||
src_filter.append(f"+<helpers/ui/OLEDDisplayFonts.cpp>")
|
||||
|
||||
# VARIANTS HANDLING
|
||||
elif isinstance(item, tuple) and item[0] == "MC_VARIANT":
|
||||
variant_name = item[1]
|
||||
src_filter.append(f"+<../variants/{variant_name}>")
|
||||
|
||||
# INCLUDE EXAMPLE CODE IN BUILD (to provide your own support files without touching the tree)
|
||||
elif isinstance(item, tuple) and item[0] == "BUILD_EXAMPLE":
|
||||
example_name = item[1]
|
||||
src_filter.append(f"+<../examples/{example_name}/*.cpp>")
|
||||
|
||||
# EXCLUDE A SOURCE FILE FROM AN EXAMPLE (must be placed after example name or boom)
|
||||
elif isinstance(item, tuple) and item[0] == "EXCLUDE_FROM_EXAMPLE":
|
||||
exclude_name = item[1]
|
||||
if example_name is None:
|
||||
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
|
||||
break
|
||||
src_filter.append(f"-<../examples/{example_name}/{exclude_name}>")
|
||||
|
||||
# DEAL WITH UI VARIANT FOR AN EXAMPLE
|
||||
elif isinstance(item, tuple) and item[0] == "MC_UI_FLAVOR":
|
||||
ui_flavor = item[1]
|
||||
if example_name is None:
|
||||
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
|
||||
break
|
||||
src_filter.append(f"+<../examples/{example_name}/{ui_flavor}/*.cpp>")
|
||||
|
||||
menv.Replace(SRC_FILTER=src_filter)
|
||||
|
||||
#print (menv.Dump())
|
||||
@@ -4,6 +4,7 @@ in
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.platformio
|
||||
pkgs.python3
|
||||
# optional: needed as a programmer i.e. for esp32
|
||||
pkgs.avrdude
|
||||
];
|
||||
|
||||
192
docs/faq.md
192
docs/faq.md
@@ -1,7 +1,7 @@
|
||||
**MeshCore-FAQ**<!-- omit from toc -->
|
||||
A list of frequently-asked questions and answers for MeshCore
|
||||
|
||||
The current version of this MeshCore FAQ is at https://github.com/ripplebiz/MeshCore/blob/main/docs/faq.md.
|
||||
The current version of this MeshCore FAQ is at https://github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md.
|
||||
This MeshCore FAQ is also mirrored at https://github.com/LitBomb/MeshCore-FAQ and might have newer updates if pull requests on Scott's MeshCore repo are not approved yet.
|
||||
|
||||
author: https://github.com/LitBomb<!-- omit from toc -->
|
||||
@@ -27,18 +27,20 @@ author: https://github.com/LitBomb<!-- omit from toc -->
|
||||
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
|
||||
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
|
||||
- [4. T-Deck Related](#4-t-deck-related)
|
||||
- [4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#41-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
|
||||
- [4.2. Q: Why is my T-Deck Plus not getting any satellite lock?](#42-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
|
||||
- [4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#43-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
|
||||
- [4.4. Q: What size of SD card does the T-Deck support?](#44-q-what-size-of-sd-card-does-the-t-deck-support)
|
||||
- [4.5. Q: What is the public key for the default public channel?](#45-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [4.6. Q: How do I get maps on T-Deck?](#46-q-how-do-i-get-maps-on-t-deck)
|
||||
- [4.7. Q: Where do the map tiles go?](#47-q-where-do-the-map-tiles-go)
|
||||
- [4.8. Q: How to unlock deeper map zoom and server management features on T-Deck?](#48-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
|
||||
- [4.9. Q: How to decipher the diagnostics screen on T-Deck?](#49-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
|
||||
- [4.10. Q: The T-Deck sound is too loud?](#410-q-the-t-deck-sound-is-too-loud)
|
||||
- [4.11. Q: Can you customize the sound?](#411-q-can-you-customize-the-sound)
|
||||
- [4.12. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#412-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
|
||||
- [4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?](#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro)
|
||||
- [4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?](#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode)
|
||||
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
|
||||
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
|
||||
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
|
||||
- [4.6. Q: what is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
|
||||
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
|
||||
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
|
||||
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
|
||||
- [4.10. Q: How to decipher the diagnostics screen on T-Deck?](#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck)
|
||||
- [4.11. Q: The T-Deck sound is too loud?](#411-q-the-t-deck-sound-is-too-loud)
|
||||
- [4.12. Q: Can you customize the sound?](#412-q-can-you-customize-the-sound)
|
||||
- [4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?](#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts)
|
||||
- [4.14. Q: How to capture a screenshot on T-Deck?](#414-q-how-to-capture-a-screenshot-on-t-deck)
|
||||
- [5. General](#5-general)
|
||||
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
|
||||
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
|
||||
@@ -63,15 +65,18 @@ author: https://github.com/LitBomb<!-- omit from toc -->
|
||||
- [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago)
|
||||
- [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed)
|
||||
- [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth)
|
||||
- [6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
|
||||
- [6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
|
||||
- [6.6. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
|
||||
- [6.7. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
|
||||
- [6.4. Q: My companion isn't showing up over Bluetooth?](#64-q-my-companion-isnt-showing-up-over-bluetooth)
|
||||
- [6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code)
|
||||
- [6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection)
|
||||
- [6.7. Q: My RAK/T1000-E/xiao\_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?](#66-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh)
|
||||
- [6.8. Q: WebFlasher fails on Linux with failed to open](#67-q-webflasher-fails-on-linux-with-failed-to-open)
|
||||
- [7. Other Questions:](#7-other-questions)
|
||||
- [7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
|
||||
- [7.2 Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
|
||||
- [7.3 Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
|
||||
- [7.4 Q are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
|
||||
- [7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app)
|
||||
- [7.2. Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air)
|
||||
- [7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?](#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu)
|
||||
- [7.4. Q: are the MeshCore logo and font available?](#74-q-are-the-meshcore-logo-and-font-available)
|
||||
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
|
||||
- [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-comnpanion-via-wifi-eg-using-a-heltec-v3)
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
@@ -97,7 +102,7 @@ Anyone is able to build anything they like on top of MeshCore without paying any
|
||||
Main web site: [https://meshcore.co.uk/](https://meshcore.co.uk/)
|
||||
Firmware Flasher: https://flasher.meshcore.co.uk/
|
||||
Phone Client Applications: https://meshcore.co.uk/apps.html
|
||||
MeshCore Fimrware GitHub: https://github.com/ripplebiz/MeshCore
|
||||
MeshCore Firmware GitHub: https://github.com/ripplebiz/MeshCore
|
||||
|
||||
NOTE: Andy Kirby has a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) for beginners.
|
||||
|
||||
@@ -105,9 +110,11 @@ Anyone is able to build anything they like on top of MeshCore without paying any
|
||||
You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).
|
||||
|
||||
#### 1.2.1. Hardware
|
||||
To use MeshCore without using a phone as the client interface, you can run MeshCore on a T-Deck or T-Deck Plus. It is a complete off-grid secure communication solution.
|
||||
MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.
|
||||
|
||||
MeshCore is also available on a variety of 868MHz and 915MHz LoRa devices. For example, RAK4631 devices (19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Seeed Studio T1000-E. More devices will be supported later.
|
||||
For an up-to-date list of supported devices, please go to https://flasher.meshcore.co.uk/
|
||||
|
||||
To use MeshCore without using a phone as the client interface, you can run MeshCore on a LiLygo's T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices are a complete off-grid secure communication solution.
|
||||
|
||||
#### 1.2.2. Firmware
|
||||
MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:
|
||||
@@ -116,7 +123,7 @@ MeshCore has four firmware types that are not available on other LoRa systems. M
|
||||
Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:
|
||||
|
||||
1. **BLE Companion**
|
||||
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android MeshCore client over BLE (iOS MeshCore client will be available soon)
|
||||
BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE
|
||||
<https://meshcore.co.uk/apps.html>
|
||||
|
||||
2. **USB Serial Companion**
|
||||
@@ -138,26 +145,28 @@ A room server can be remotely administered using a T-Deck running the MeshCore f
|
||||
|
||||
When a client logs into a room server, the client will receive the previously 32 unseen messages.
|
||||
|
||||
A room server can also take on the repeater role. To enable repeater role on a room server, use this command:
|
||||
Although room server can also repeat with the command line command `set repeat on`, it is not recommended nor encouraged. A room server with repeat set to `on` lacks the full set of repeater and remote administration features that are only available in the repeater firmware.
|
||||
|
||||
The recommendation is to run repeater and room server on separate devices for the best experience.
|
||||
|
||||
|
||||
`set repeat {on|off}`
|
||||
|
||||
---
|
||||
|
||||
## 2. Initial Setup
|
||||
|
||||
### 2.1. Q: How many devices do I need to start using MeshCore?
|
||||
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via Bluetooth (iOS client will be available later). You can start communicating with other MeshCore users near you.
|
||||
**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.
|
||||
|
||||
If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your near-by friends and family.
|
||||
|
||||
If you have two supported devices, and there are other MeshcCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
|
||||
If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.
|
||||
|
||||
After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:
|
||||
|
||||
`set freq {frequency}`
|
||||
|
||||
The repeater and room server CLI reference is here: https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
The repeater and room server CLI reference is here: https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
|
||||
If you have more supported devices, you can use your additional devices with the room server firmware.
|
||||
|
||||
@@ -197,10 +206,12 @@ MeshCore allows you to manually broadcast your name, position and public encrypt
|
||||
* Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it.
|
||||
* Flooded means it's broadcasted out and then repeated by all the repeaters that hear it.
|
||||
|
||||
MeshCore clients only advertise themselves when the user initiates it. A repeater (and room server?) advertises its presence once every 240 minutes. This interval can be configured using the following command:
|
||||
MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command:
|
||||
|
||||
`set advert.interval {minutes}`
|
||||
|
||||
As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts.
|
||||
|
||||
### 2.5. Q: Is there a hop limit?
|
||||
|
||||
**A:** Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.
|
||||
@@ -219,8 +230,7 @@ Repeater or room server can be administered with one of the options below:
|
||||
|
||||
- After a repeater or room server firmware is flashed on to a LoRa device, go to <https://config.meshcore.dev> and use the web user interface to connect to the LoRa device via USB serial. From there you can set the name of the server, its frequency and other related settings, location, passwords etc.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- Connect the server device using a USB cable to a computer running Chrome on https://flasher.meshcore.co.uk/, then use the `console` feature to connect to the device
|
||||
|
||||
@@ -255,7 +265,11 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
|
||||
|
||||
## 4. T-Deck Related
|
||||
|
||||
### 4.1. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
|
||||
### 4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?
|
||||
|
||||
**A:** Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users
|
||||
|
||||
### 4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?
|
||||
**A:**
|
||||
1. Device off
|
||||
2. Connect USB cable to device
|
||||
@@ -266,20 +280,20 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
|
||||
7. T-Deck in DFU mode now
|
||||
8. At this point you can begin flashing using <https://flasher.meshcore.co.uk/>
|
||||
|
||||
### 4.2. Q: Why is my T-Deck Plus not getting any satellite lock?
|
||||
### 4.3. Q: Why is my T-Deck Plus not getting any satellite lock?
|
||||
**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.
|
||||
|
||||
GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-Deck will continue to try to get a GPS lock. You can go to the `GPS Info` screen; you should see the `Sentences:` counter increasing if the baud rate is correct.
|
||||
|
||||
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689)
|
||||
|
||||
### 4.3. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
|
||||
### 4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?
|
||||
**A:** The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.
|
||||
|
||||
### 4.4. Q: What size of SD card does the T-Deck support?
|
||||
### 4.5. Q: What size of SD card does the T-Deck support?
|
||||
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
|
||||
|
||||
### 4.5. Q: what is the public key for the default public channel?
|
||||
### 4.6. Q: what is the public key for the default public channel?
|
||||
**A:**
|
||||
T-Deck uses the same key the smartphone apps use but in base64
|
||||
`izOH6cXN6mrJ5e26oRXNcg==`
|
||||
@@ -290,7 +304,7 @@ The smartphone app key is in hex:
|
||||
|
||||
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354194409213792388)
|
||||
|
||||
### 4.6. Q: How do I get maps on T-Deck?
|
||||
### 4.7. Q: How do I get maps on T-Deck?
|
||||
**A:** You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):
|
||||
- <https://buymeacoffee.com/ripplebiz/e/342543> (Europe)
|
||||
- <https://buymeacoffee.com/ripplebiz/e/342542> (US)
|
||||
@@ -304,19 +318,20 @@ There is also a modified script that adds additional error handling and parallel
|
||||
UK map tiles are available separately from Andy Kirby on his discord server:
|
||||
<https://discord.com/channels/826570251612323860/1330643963501351004/1331346597367386224>
|
||||
|
||||
### 4.7. Q: Where do the map tiles go?
|
||||
### 4.8. Q: Where do the map tiles go?
|
||||
Once you have the tiles downloaded, copy the `\tiles` folder to the root of your T-Deck's SD card.
|
||||
|
||||
### 4.8. Q: How to unlock deeper map zoom and server management features on T-Deck?
|
||||
### 4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?
|
||||
**A:** You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \$10 per T-Deck device.
|
||||
Unlock page: <https://buymeacoffee.com/ripplebiz/e/249834>
|
||||
|
||||
### 4.9. Q: How to decipher the diagnostics screen on T-Deck?
|
||||
### 4.10. Q: How to decipher the diagnostics screen on T-Deck?
|
||||
|
||||
**A: ** Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is :
|
||||
`{hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}`
|
||||
|
||||
See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19](https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19 "https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19")
|
||||
See here for packet-type:
|
||||
https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19
|
||||
|
||||
|
||||
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
@@ -330,15 +345,25 @@ See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/P
|
||||
|
||||
[Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350611321040932966)
|
||||
|
||||
### 4.10. Q: The T-Deck sound is too loud?
|
||||
### 4.11. Q: Can you customize the sound?
|
||||
### 4.11. Q: The T-Deck sound is too loud?
|
||||
### 4.12. Q: Can you customize the sound?
|
||||
|
||||
**A:** You can customise the sounds on the T-Deck, just by placing `.mp3` files onto the `root` dir of the SD card. `startup.mp3`, `alert.mp3` and `new-advert.mp3`
|
||||
**A:** You can customise the sounds on the T-Deck, by placing `.mp3` files onto the `root` dir of the SD card. The files are:
|
||||
|
||||
### 4.12. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
|
||||
* `startup.mp3`
|
||||
* `error.mp3`
|
||||
* `alert.mp3`
|
||||
* `new-advert.mp3`
|
||||
* `existing-advert.mp3`
|
||||
|
||||
### 4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?
|
||||
|
||||
**A:** 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with "meshcore://...")
|
||||
|
||||
### 4.14. Q: How to capture a screenshot on T-Deck?
|
||||
|
||||
**A:** To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.
|
||||
|
||||
---
|
||||
|
||||
## 5. General
|
||||
@@ -367,7 +392,7 @@ In MeshCore, only repeaters and room server with `set repeat on` repeat.
|
||||
|
||||
**A:** If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.
|
||||
|
||||
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to reestablish a path.
|
||||
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
|
||||
|
||||
### 5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
|
||||
|
||||
@@ -393,7 +418,7 @@ The third character is the capital letter 'O', not zero `0`
|
||||
|
||||
### 5.7. Q: Is MeshCore open source?
|
||||
**A:** Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.
|
||||
- Firmware repo: <https://github.com/ripplebiz/MeshCore>
|
||||
- Firmware repo: https://github.com/meshcore-dev/MeshCore
|
||||
|
||||
### 5.8. Q: How can I support MeshCore?
|
||||
**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at <https://buymeacoffee.com/ripplebiz>.
|
||||
@@ -439,7 +464,7 @@ Andy also has a video on how to build using VS Code:
|
||||
|
||||
### 5.10. Q: Are there other MeshCore related open source projects?
|
||||
|
||||
**A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript libary are open source under MIT license.
|
||||
**A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript library are open source under MIT license.
|
||||
|
||||
Web client: https://github.com/liamcottle/meshcore-web
|
||||
Javascript: https://github.com/liamcottle/meshcore.js
|
||||
@@ -447,7 +472,7 @@ Javascript: https://github.com/liamcottle/meshcore.js
|
||||
### 5.11. Q: Does MeshCore support ATAK
|
||||
**A:** ATAK is not currently on MeshCore's roadmap.
|
||||
|
||||
Meshcore would not be best suited to ATAK because MeshCore:
|
||||
Meshcore would not be best suited to ATAK because MeshCore:
|
||||
clients do not repeat and therefore you would need a network of repeaters in place
|
||||
will not have a stable path where all clients are constantly moving between repeaters
|
||||
|
||||
@@ -457,10 +482,14 @@ This could change in the future if MeshCore develops a client firmware that repe
|
||||
[Source](https://discord.com/channels/826570251612323860/1330643963501351004/1354780032140054659)
|
||||
|
||||
### 5.12. Q: How do I add a node to the [MeshCore Map]([url](https://meshcore.co.uk/map.html))
|
||||
**A:** From the smartphone app, connect to a BLE Companion radio
|
||||
- To add the BLE Companion radio your smartphone is connected to to the map, tap the `advert` icon, then tap `Advert (To Clipboard)`.
|
||||
- To add a Repeater or Room Server to the map, tap the 3 dots next to the Repeater or Room Server you want to add to the map, then tap `Share (To Clipboard)`.
|
||||
- Go to the [MeshCore Map web site]([url](https://meshcore.co.uk/map.html)), tap the plus sign on the lower right corner and paste in the meshcore://... blob, then tap `Add Node`
|
||||
**A:**
|
||||
|
||||
To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the `3 dot` menu icon at the top right corner, then tap `Internet Map`. Tap the `3 dot` menu icon again and choose `Add me to the Map`
|
||||
|
||||
To add a Repeater or Room Server to the map, go to the Contact List, tap the `3 dot` next to the Repeater or Room Server you want to add to the Internet Map, tap `Share`, then tap `Upload to Internet Map`.
|
||||
|
||||
You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.
|
||||
|
||||
|
||||
### 5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?
|
||||
** A:** Yes.
|
||||
@@ -521,7 +550,7 @@ To start managing your USB serial-connected device using picocom, use the follow
|
||||
- `picocom -b 115200 /dev/ttyUSB0 --imap lfcrlf`
|
||||
|
||||
From here, reference repeater and room server command line commands on MeshCore github wiki here:
|
||||
- https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
- https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
|
||||
|
||||
### 5.14. Q: Are there are projects built around MeshCore?
|
||||
@@ -541,7 +570,7 @@ Bindings to access your MeshCore companion radio nodes in python.
|
||||
https://github.com/fdlamotte/meshcore_py
|
||||
|
||||
#### 5.14.4. meshcore-cli
|
||||
CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Pyton MeshCore above.
|
||||
CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Python MeshCore above.
|
||||
https://github.com/fdlamotte/meshcore-cli
|
||||
|
||||
#### 5.14.5. meshcore.js
|
||||
@@ -563,15 +592,19 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
|
||||
### 6.3. Q: How to connect to a repeater via BLE (Bluetooth)?
|
||||
**A:** You can't connect to a device running repeater firmware via Bluetooth. Devices running the BLE companion firmware you can connect to it via Bluetooth using the android app
|
||||
|
||||
### 6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
|
||||
### 6.4. Q: My companion isn't showing up over Bluetooth?
|
||||
|
||||
**A:** make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.
|
||||
|
||||
### 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?
|
||||
|
||||
**A:** the default Bluetooth pairing code is `123456`
|
||||
|
||||
### 6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.
|
||||
### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.
|
||||
|
||||
**A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.
|
||||
|
||||
### 6.6. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
|
||||
### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?
|
||||
|
||||
**A:**
|
||||
1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
|
||||
@@ -591,8 +624,7 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
|
||||
|
||||
Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by hold down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on flasher.meshcore.co.uk
|
||||
|
||||
|
||||
### 6.7. Q: WebFlasher fails on Linux with failed to open
|
||||
### 6.8. Q: WebFlasher fails on Linux with failed to open
|
||||
|
||||
**A:** If the usb port doesn't have the right ownership for this task, the process fails with the following error:
|
||||
`NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.`
|
||||
@@ -603,30 +635,30 @@ Allow the browser user on it:
|
||||
---
|
||||
## 7. Other Questions:
|
||||
|
||||
### 7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?
|
||||
### 7.1. Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?
|
||||
|
||||
**A:** The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:
|
||||
|
||||
1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for `nrf dfu`, the app's full name is `nRF Device Firmware Update`
|
||||
2. On flasher.meshcore.co.uk, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
|
||||
3. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
|
||||
3. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
|
||||
4. Go to the Command Line tab, type `start ota` and hit enter.
|
||||
5. you should see `OK` to confirm the repeater device is now in OTA mode
|
||||
6. Run the DFU app,tab `Settings` on the top right corner
|
||||
7. Enable `Packets receipt notifications`, and change `Number of Packets` to 10 for RAK, 8 for T114. 8 also works for RAK.
|
||||
9. Select the firmware zip file you downloaded
|
||||
10. Select the device you want to update. If the device you want to updat is not on the list, try enabling`OTA` on the device again
|
||||
10. Select the device you want to update. If the device you want to update is not on the list, try enabling`OTA` on the device again
|
||||
11. If the device is not found, enable `Force Scanning` in the DFU app
|
||||
12. Tab the `Upload` to begin OTA update
|
||||
13. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone.
|
||||
14. Wait for the update to complete. It can take a few minutes.
|
||||
|
||||
|
||||
### 7.2 Q: How to update ESP32-based devices over the air?
|
||||
### 7.2. Q: How to update ESP32-based devices over the air?
|
||||
|
||||
**A:** For ESP32-based devices (e.g. Heltec V3):
|
||||
1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name)
|
||||
2. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge
|
||||
2. From the MeshCore app, login remotely to the repeater you want to update with admin privilege
|
||||
4. Go to the Command Line tab, type `start ota` and hit enter.
|
||||
5. you should see `OK` to confirm the repeater device is now in OTA mode
|
||||
6. The command `start ota` on an ESP32-based device starts a wifi hotspot named `MeshCore OTA`
|
||||
@@ -634,7 +666,7 @@ Allow the browser user on it:
|
||||
8. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher
|
||||
|
||||
|
||||
### 7.3 Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
|
||||
### 7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?
|
||||
|
||||
**A:** Yes, developer `che aporeps` has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.
|
||||
|
||||
@@ -646,9 +678,29 @@ Currently, the following boards are supported:
|
||||
- Seeed Studio XIAO nRF52840 BLE SENSE
|
||||
- RAK 4631
|
||||
|
||||
### 7.4 Q are the MeshCore logo and font available?
|
||||
### 7.4. Q: are the MeshCore logo and font available?
|
||||
|
||||
**A:** Yes, it is on the MeshCore github repo here: https://github.com/ripplebiz/MeshCore/tree/main/logo
|
||||
**A:** Yes, it is on the MeshCore github repo here:
|
||||
https://github.com/meshcore-dev/MeshCore/tree/main/logo
|
||||
|
||||
### 7.5. Q: What is the format of a contact or channel QR code?
|
||||
|
||||
**A:**
|
||||
Channel:
|
||||
`meshcore://channel/add?name=<name>&secret=<secret>`
|
||||
|
||||
Contact:
|
||||
`meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>`
|
||||
|
||||
where `&type` is:
|
||||
`chat = 1`
|
||||
`repeater = 2`
|
||||
`room = 3`
|
||||
`sensor = 4`
|
||||
|
||||
### 7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?
|
||||
**A:**
|
||||
WiFi firmware requires you to compile it yourself, as you need to set the wifi ssid and password.
|
||||
Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then flash it to your device.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Packet Structure
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------|----------------------------------|-----------------------------------------------------------|
|
||||
| header | 1 | Contains routing type, payload type, and payload version. |
|
||||
| path_len | 1 | Length of the path field in bytes. |
|
||||
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
|
||||
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
|
||||
| Field | Size (bytes) | Description |
|
||||
|-----------------|----------------------------------|-----------------------------------------------------------|
|
||||
| header | 1 | Contains routing type, payload type, and payload version. |
|
||||
| transport_codes | 4 (optional) | 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) |
|
||||
| path_len | 1 | Length of the path field in bytes. |
|
||||
| path | up to 64 (`MAX_PATH_SIZE`) | Stores the routing path if applicable. |
|
||||
| payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. |
|
||||
|
||||
Note: see the [payloads doc](./payloads.md) for more information about the content of payload.
|
||||
|
||||
@@ -42,6 +43,7 @@ bit 0 means the lowest bit (1s place)
|
||||
| `0x07` | `PAYLOAD_TYPE_ANON_REQ` | Anonymous request. |
|
||||
| `0x08` | `PAYLOAD_TYPE_PATH` | Returned path. |
|
||||
| `0x09` | `PAYLOAD_TYPE_TRACE` | trace a path, collecting SNI for each hop. |
|
||||
| `0x0A` | `PAYLOAD_TYPE_MULTIPART` | packet is part of a sequence of packets. |
|
||||
| `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
|
||||
|
||||
## Payload Version Values
|
||||
|
||||
@@ -10,13 +10,16 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
|
||||
* Anonymous request.
|
||||
* Group text message (unverified).
|
||||
* Group datagram (unverified).
|
||||
* Multi-part packet
|
||||
* Custom packet (raw bytes, custom encryption).
|
||||
|
||||
This document defines the structure of each of these payload types
|
||||
This document defines the structure of each of these payload types.
|
||||
|
||||
NOTE: all 16 and 32-bit integer fields are Little Endian.
|
||||
|
||||
## Important concepts:
|
||||
|
||||
* Node/channel hash: the first byte of the node or channel's public key
|
||||
* Node hash: the first byte of the node's public key
|
||||
|
||||
# Node advertisement
|
||||
This kind of payload notifies receivers that a node exists, and gives information about the node
|
||||
@@ -33,10 +36,10 @@ Appdata
|
||||
| Field | Size (bytes) | Description |
|
||||
|---------------|-----------------|-------------------------------------------------------|
|
||||
| flags | 1 | specifies which of the fields are present, see below |
|
||||
| latitude | 4 | decimal latitude multiplied by 1000000, integer |
|
||||
| longitude | 4 | decimal longitude multiplied by 1000000, integer |
|
||||
| feature 1 | 2 | reserved for future use |
|
||||
| feature 2 | 2 | reserved for future use |
|
||||
| latitude | 4 (optional) | decimal latitude multiplied by 1000000, integer |
|
||||
| longitude | 4 (optional) | decimal longitude multiplied by 1000000, integer |
|
||||
| feature 1 | 2 (optional) | reserved for future use |
|
||||
| feature 2 | 2 (optional) | reserved for future use |
|
||||
| name | rest of appdata | name of the node |
|
||||
|
||||
Appdata Flags
|
||||
@@ -46,6 +49,7 @@ Appdata Flags
|
||||
| `0x01` | is chat node | advert is for a chat node |
|
||||
| `0x02` | is repeater | advert is for a repeater |
|
||||
| `0x03` | is room server | advert is for a room server |
|
||||
| `0x04` | is sensor | advert is for a sensor server |
|
||||
| `0x10` | has location | appdata contains lat/long information |
|
||||
| `0x20` | has feature 1 | Reserved for future use. |
|
||||
| `0x40` | has feature 2 | Reserved for future use. |
|
||||
@@ -92,13 +96,15 @@ Returned path messages provide a description of the route a packet took from the
|
||||
|
||||
Request type
|
||||
|
||||
| Value | Name | Description |
|
||||
|--------|--------------------|---------------------------------------|
|
||||
| `0x01` | get status | get status of repeater or room server |
|
||||
| `0x02` | keepalive | TODO |
|
||||
| `0x03` | get telemetry data | TODO |
|
||||
| Value | Name | Description |
|
||||
|--------|----------------------|---------------------------------------|
|
||||
| `0x01` | get stats | get stats of repeater or room server |
|
||||
| `0x02` | keepalive | (deprecated) |
|
||||
| `0x03` | get telemetry data | TODO |
|
||||
| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
|
||||
| `0x05` | get access list | get node's approved access list |
|
||||
|
||||
### Get status
|
||||
### Get stats
|
||||
|
||||
Gets information about the node, possibly including the following:
|
||||
|
||||
@@ -121,10 +127,6 @@ Gets information about the node, possibly including the following:
|
||||
* Number posted (?)
|
||||
* Number of post pushes (?)
|
||||
|
||||
### Keepalive
|
||||
|
||||
No-op request.
|
||||
|
||||
### Get telemetry data
|
||||
|
||||
Request data about sensors on the node, including battery level.
|
||||
@@ -138,11 +140,11 @@ Request data about sensors on the node, including battery level.
|
||||
|
||||
## Plain text message
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|--------------|-----------------|--------------------------------------------------------------|
|
||||
| timestamp | 4 | send time (unix timestamp) |
|
||||
| flags + TODO | 1 | first six bits are flags (see below), last two bits are TODO |
|
||||
| message | rest of payload | the message content, see next table |
|
||||
| Field | Size (bytes) | Description |
|
||||
|-----------------|-----------------|--------------------------------------------------------------|
|
||||
| timestamp | 4 | send time (unix timestamp) |
|
||||
| flags + attempt | 1 | upper six bits are flags (see below), lower two bits are attempt number (0..3) |
|
||||
| message | rest of payload | the message content, see next table |
|
||||
|
||||
Flags
|
||||
|
||||
@@ -150,7 +152,7 @@ Flags
|
||||
|--------|---------------------------|------------------------------------------------------------|
|
||||
| `0x00` | plain text message | the plain text of the message |
|
||||
| `0x01` | CLI command | the command text of the message |
|
||||
| `0x02` | signed plain text message | two bytes of sender prefix, followed by plain text message |
|
||||
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
|
||||
|
||||
# Anonymous request
|
||||
|
||||
@@ -166,14 +168,14 @@ Plaintext message
|
||||
| Field | Size (bytes) | Description |
|
||||
|----------------|-----------------|-------------------------------------------------------------------------------|
|
||||
| timestamp | 4 | send time (unix timestamp) |
|
||||
| sync timestamp | 4 | for room server, otherwise absent: sender's "sync messages SINCE x" timestamp |
|
||||
| sync timestamp | 4 | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
|
||||
| password | rest of message | password for repeater/room |
|
||||
|
||||
# Group text message / datagram
|
||||
|
||||
| Field | Size (bytes) | Description |
|
||||
|--------------|-----------------|--------------------------------------------|
|
||||
| channel hash | 1 | the first byte of the channel's public key |
|
||||
| channel hash | 1 | first byte of SHA256 of channel's shared key |
|
||||
| cipher MAC | 2 | MAC for encrypted data in next field |
|
||||
| ciphertext | rest of payload | encrypted message, see below for details |
|
||||
|
||||
|
||||
46
examples/companion_radio/AbstractUITask.h
Normal file
46
examples/companion_radio/AbstractUITask.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/BaseSerialInterface.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
#include <helpers/ui/buzzer.h>
|
||||
#endif
|
||||
|
||||
#include "NodePrefs.h"
|
||||
|
||||
enum class UIEventType {
|
||||
none,
|
||||
contactMessage,
|
||||
channelMessage,
|
||||
roomMessage,
|
||||
newContactMessage,
|
||||
ack
|
||||
};
|
||||
|
||||
class AbstractUITask {
|
||||
protected:
|
||||
mesh::MainBoard* _board;
|
||||
BaseSerialInterface* _serial;
|
||||
bool _connected;
|
||||
|
||||
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) {
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
public:
|
||||
void setHasConnection(bool connected) { _connected = connected; }
|
||||
bool hasConnection() const { return _connected; }
|
||||
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
|
||||
bool isSerialEnabled() const { return _serial->isEnabled(); }
|
||||
void enableSerial() { _serial->enable(); }
|
||||
void disableSerial() { _serial->disable(); }
|
||||
virtual void msgRead(int msgcount) = 0;
|
||||
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
|
||||
virtual void notify(UIEventType t = UIEventType::none) = 0;
|
||||
virtual void loop() = 0;
|
||||
};
|
||||
@@ -1,7 +1,13 @@
|
||||
#include <Arduino.h>
|
||||
#include "DataStore.h"
|
||||
|
||||
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&clock),
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
#define MAX_BLOBRECS 100
|
||||
#else
|
||||
#define MAX_BLOBRECS 20
|
||||
#endif
|
||||
|
||||
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
identity_store(fs, "")
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
@@ -12,24 +18,45 @@ DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&
|
||||
{
|
||||
}
|
||||
|
||||
static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
_fs->remove(filename);
|
||||
return _fs->open(filename, FILE_O_WRITE);
|
||||
identity_store(fs, "")
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(filename, "w");
|
||||
identity_store(fs, "/identity")
|
||||
#else
|
||||
return _fs->open(filename, "w", true);
|
||||
identity_store(fs, "/identity")
|
||||
#endif
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
static File openWrite(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
fs->remove(filename);
|
||||
return fs->open(filename, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "w");
|
||||
#else
|
||||
return fs->open(filename, "w", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
static uint32_t _ContactsChannelsTotalBlocks = 0;
|
||||
#endif
|
||||
|
||||
void DataStore::begin() {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
identity_store.begin();
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
|
||||
checkAdvBlobFile();
|
||||
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
||||
migrateToSecondaryFS();
|
||||
#endif
|
||||
#else
|
||||
// init 'blob store' support
|
||||
_fs->mkdir("/bl");
|
||||
@@ -41,19 +68,33 @@ void DataStore::begin() {
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#if defined(QSPIFLASH)
|
||||
#include <CustomLFS_QSPIFlash.h>
|
||||
#elif defined(EXTRAFS)
|
||||
#include <CustomLFS.h>
|
||||
#else
|
||||
#include <InternalFileSystem.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
int _countLfsBlock(void *p, lfs_block_t block){
|
||||
if (block > _ContactsChannelsTotalBlocks) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
|
||||
return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully
|
||||
}
|
||||
lfs_size_t *size = (lfs_size_t*) p;
|
||||
*size += 1;
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
lfs_ssize_t _getLfsUsedBlockCount() {
|
||||
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
|
||||
lfs_size_t size = 0;
|
||||
lfs_traverse(InternalFS._getFS(), _countLfsBlock, &size);
|
||||
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size);
|
||||
if (err) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
|
||||
return 0;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -67,8 +108,8 @@ uint32_t DataStore::getStorageUsedKb() const {
|
||||
_fs->info(info);
|
||||
return info.usedBytes / 1024;
|
||||
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
const lfs_config* config = InternalFS._getFS()->cfg;
|
||||
int usedBlockCount = _getLfsUsedBlockCount();
|
||||
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
||||
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
|
||||
int usedBytes = config->block_size * usedBlockCount;
|
||||
return usedBytes / 1024;
|
||||
#else
|
||||
@@ -85,7 +126,7 @@ uint32_t DataStore::getStorageTotalKb() const {
|
||||
_fs->info(info);
|
||||
return info.totalBytes / 1024;
|
||||
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
const lfs_config* config = InternalFS._getFS()->cfg;
|
||||
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
||||
int totalBytes = config->block_size * config->block_count;
|
||||
return totalBytes / 1024;
|
||||
#else
|
||||
@@ -103,13 +144,31 @@ File DataStore::openRead(const char* filename) {
|
||||
#endif
|
||||
}
|
||||
|
||||
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return fs->open(filename, FILE_O_READ);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "r");
|
||||
#else
|
||||
return fs->open(filename, "r", false);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DataStore::removeFile(const char* filename) {
|
||||
return _fs->remove(filename);
|
||||
}
|
||||
|
||||
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
|
||||
return fs->remove(filename);
|
||||
}
|
||||
|
||||
bool DataStore::formatFileSystem() {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return _fs->format();
|
||||
if (_fsExtra == nullptr) {
|
||||
return _fs->format();
|
||||
} else {
|
||||
return _fs->format() && _fsExtra->format();
|
||||
}
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return LittleFS.format();
|
||||
#elif defined(ESP32)
|
||||
@@ -154,7 +213,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
|
||||
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
|
||||
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
|
||||
file.read((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
|
||||
file.read(pad, 1); // 62
|
||||
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
|
||||
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
|
||||
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
|
||||
@@ -163,7 +222,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
|
||||
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
|
||||
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
|
||||
file.read(pad, 3); // 77
|
||||
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
|
||||
file.read(pad, 2); // 78
|
||||
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
|
||||
|
||||
file.close();
|
||||
@@ -184,7 +244,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
|
||||
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
|
||||
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
|
||||
file.write((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
|
||||
file.write(pad, 1); // 62
|
||||
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
|
||||
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
|
||||
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
|
||||
@@ -193,7 +253,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
|
||||
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
|
||||
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
|
||||
file.write(pad, 3); // 77
|
||||
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
|
||||
file.write(pad, 2); // 78
|
||||
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
|
||||
|
||||
file.close();
|
||||
@@ -201,11 +262,15 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
}
|
||||
|
||||
void DataStore::loadContacts(DataStoreHost* host) {
|
||||
if (_fs->exists("/contacts3")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
if (_getContactsChannelsFS()->exists("/contacts3")) {
|
||||
File file = _getContactsChannelsFS()->open("/contacts3");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
if (_fs->exists("/contacts3")) {
|
||||
File file = _fs->open("/contacts3", "r");
|
||||
#else
|
||||
File file = _fs->open("/contacts3");
|
||||
if (_fs->exists("/contacts3")) {
|
||||
File file = _fs->open("/contacts3", "r", false);
|
||||
#endif
|
||||
if (file) {
|
||||
bool full = false;
|
||||
@@ -238,7 +303,7 @@ void DataStore::loadContacts(DataStoreHost* host) {
|
||||
}
|
||||
|
||||
void DataStore::saveContacts(DataStoreHost* host) {
|
||||
File file = openWrite(_fs, "/contacts3");
|
||||
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
|
||||
if (file) {
|
||||
uint32_t idx = 0;
|
||||
ContactInfo c;
|
||||
@@ -267,11 +332,15 @@ void DataStore::saveContacts(DataStoreHost* host) {
|
||||
}
|
||||
|
||||
void DataStore::loadChannels(DataStoreHost* host) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
if (_getContactsChannelsFS()->exists("/channels2")) {
|
||||
File file = _getContactsChannelsFS()->open("/channels2");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
if (_fs->exists("/channels2")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = _fs->open("/channels2", "r");
|
||||
#else
|
||||
File file = _fs->open("/channels2");
|
||||
if (_fs->exists("/channels2")) {
|
||||
File file = _fs->open("/channels2", "r", false);
|
||||
#endif
|
||||
if (file) {
|
||||
bool full = false;
|
||||
@@ -298,7 +367,7 @@ void DataStore::loadChannels(DataStoreHost* host) {
|
||||
}
|
||||
|
||||
void DataStore::saveChannels(DataStoreHost* host) {
|
||||
File file = openWrite(_fs, "/channels2");
|
||||
File file = openWrite(_getContactsChannelsFS(), "/channels2");
|
||||
if (file) {
|
||||
uint8_t channel_idx = 0;
|
||||
ChannelDetails ch;
|
||||
@@ -329,12 +398,12 @@ struct BlobRec {
|
||||
};
|
||||
|
||||
void DataStore::checkAdvBlobFile() {
|
||||
if (!_fs->exists("/adv_blobs")) {
|
||||
File file = openWrite(_fs, "/adv_blobs");
|
||||
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
|
||||
File file = openWrite(_getContactsChannelsFS(), "/adv_blobs");
|
||||
if (file) {
|
||||
BlobRec zeroes;
|
||||
memset(&zeroes, 0, sizeof(zeroes));
|
||||
for (int i = 0; i < 20; i++) { // pre-allocate to fixed size
|
||||
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
|
||||
file.write((uint8_t *) &zeroes, sizeof(zeroes));
|
||||
}
|
||||
file.close();
|
||||
@@ -342,10 +411,117 @@ void DataStore::checkAdvBlobFile() {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
||||
File file = _fs->open("/adv_blobs");
|
||||
uint8_t len = 0; // 0 = not found
|
||||
void DataStore::migrateToSecondaryFS() {
|
||||
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
|
||||
if (!_fsExtra->exists("/adv_blobs")) {
|
||||
if (_fs->exists("/adv_blobs")) {
|
||||
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
|
||||
File newAdvBlobs = openWrite(_fsExtra, "/adv_blobs");
|
||||
|
||||
if (oldAdvBlobs && newAdvBlobs) {
|
||||
BlobRec rec;
|
||||
size_t count = 0;
|
||||
|
||||
// Copy 20 BlobRecs from old to new
|
||||
while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
||||
newAdvBlobs.seek(count * sizeof(BlobRec));
|
||||
newAdvBlobs.write((uint8_t *)&rec, sizeof(rec));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (oldAdvBlobs) oldAdvBlobs.close();
|
||||
if (newAdvBlobs) newAdvBlobs.close();
|
||||
_fs->remove("/adv_blobs");
|
||||
}
|
||||
}
|
||||
if (!_fsExtra->exists("/contacts3")) {
|
||||
if (_fs->exists("/contacts3")) {
|
||||
File oldFile = openRead(_fs, "/contacts3");
|
||||
File newFile = openWrite(_fsExtra, "/contacts3");
|
||||
|
||||
if (oldFile && newFile) {
|
||||
uint8_t buf[64];
|
||||
int n;
|
||||
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
||||
newFile.write(buf, n);
|
||||
}
|
||||
}
|
||||
if (oldFile) oldFile.close();
|
||||
if (newFile) newFile.close();
|
||||
_fs->remove("/contacts3");
|
||||
}
|
||||
}
|
||||
if (!_fsExtra->exists("/channels2")) {
|
||||
if (_fs->exists("/channels2")) {
|
||||
File oldFile = openRead(_fs, "/channels2");
|
||||
File newFile = openWrite(_fsExtra, "/channels2");
|
||||
|
||||
if (oldFile && newFile) {
|
||||
uint8_t buf[64];
|
||||
int n;
|
||||
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
||||
newFile.write(buf, n);
|
||||
}
|
||||
}
|
||||
if (oldFile) oldFile.close();
|
||||
if (newFile) newFile.close();
|
||||
_fs->remove("/channels2");
|
||||
}
|
||||
}
|
||||
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
|
||||
if (_fsExtra->exists("/_main.id")) {
|
||||
if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");}
|
||||
File oldFile = openRead(_fsExtra, "/_main.id");
|
||||
File newFile = openWrite(_fs, "/_main.id");
|
||||
|
||||
if (oldFile && newFile) {
|
||||
uint8_t buf[64];
|
||||
int n;
|
||||
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
||||
newFile.write(buf, n);
|
||||
}
|
||||
}
|
||||
if (oldFile) oldFile.close();
|
||||
if (newFile) newFile.close();
|
||||
_fsExtra->remove("/_main.id");
|
||||
}
|
||||
if (_fsExtra->exists("/new_prefs")) {
|
||||
if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");}
|
||||
File oldFile = openRead(_fsExtra, "/new_prefs");
|
||||
File newFile = openWrite(_fs, "/new_prefs");
|
||||
|
||||
if (oldFile && newFile) {
|
||||
uint8_t buf[64];
|
||||
int n;
|
||||
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
||||
newFile.write(buf, n);
|
||||
}
|
||||
}
|
||||
if (oldFile) oldFile.close();
|
||||
if (newFile) newFile.close();
|
||||
_fsExtra->remove("/new_prefs");
|
||||
}
|
||||
// remove files from where they should not be anymore
|
||||
if (_fs->exists("/adv_blobs")) {
|
||||
_fs->remove("/adv_blobs");
|
||||
}
|
||||
if (_fs->exists("/contacts3")) {
|
||||
_fs->remove("/contacts3");
|
||||
}
|
||||
if (_fs->exists("/channels2")) {
|
||||
_fs->remove("/channels2");
|
||||
}
|
||||
if (_fsExtra->exists("/_main.id")) {
|
||||
_fsExtra->remove("/_main.id");
|
||||
}
|
||||
if (_fsExtra->exists("/new_prefs")) {
|
||||
_fsExtra->remove("/new_prefs");
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
||||
File file = _getContactsChannelsFS()->open("/adv_blobs");
|
||||
uint8_t len = 0; // 0 = not found
|
||||
if (file) {
|
||||
BlobRec tmp;
|
||||
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
|
||||
@@ -362,10 +538,8 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
|
||||
|
||||
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
|
||||
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
|
||||
|
||||
checkAdvBlobFile();
|
||||
|
||||
File file = _fs->open("/adv_blobs", FILE_O_WRITE);
|
||||
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
|
||||
if (file) {
|
||||
uint32_t pos = 0, found_pos = 0;
|
||||
uint32_t min_timestamp = 0xFFFFFFFF;
|
||||
|
||||
@@ -15,6 +15,7 @@ public:
|
||||
|
||||
class DataStore {
|
||||
FILESYSTEM* _fs;
|
||||
FILESYSTEM* _fsExtra;
|
||||
mesh::RTCClock* _clock;
|
||||
IdentityStore identity_store;
|
||||
|
||||
@@ -25,8 +26,11 @@ class DataStore {
|
||||
|
||||
public:
|
||||
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
|
||||
DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock);
|
||||
void begin();
|
||||
bool formatFileSystem();
|
||||
FILESYSTEM* getPrimaryFS() const { return _fs; }
|
||||
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
|
||||
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
||||
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
||||
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
||||
@@ -35,10 +39,16 @@ public:
|
||||
void saveContacts(DataStoreHost* host);
|
||||
void loadChannels(DataStoreHost* host);
|
||||
void saveChannels(DataStoreHost* host);
|
||||
void migrateToSecondaryFS();
|
||||
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
|
||||
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
|
||||
File openRead(const char* filename);
|
||||
File openRead(FILESYSTEM* fs, const char* filename);
|
||||
bool removeFile(const char* filename);
|
||||
bool removeFile(FILESYSTEM* fs, const char* filename);
|
||||
uint32_t getStorageUsedKb() const;
|
||||
uint32_t getStorageTotalKb() const;
|
||||
|
||||
private:
|
||||
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
#define CMD_SEND_TRACE_PATH 36
|
||||
#define CMD_SET_DEVICE_PIN 37
|
||||
#define CMD_SET_OTHER_PARAMS 38
|
||||
#define CMD_SEND_TELEMETRY_REQ 39
|
||||
#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this
|
||||
#define CMD_GET_CUSTOM_VARS 40
|
||||
#define CMD_SET_CUSTOM_VAR 41
|
||||
#define CMD_GET_ADVERT_PATH 42
|
||||
@@ -49,6 +49,7 @@
|
||||
// NOTE: CMD range 44..49 parked, potentially for WiFi operations
|
||||
#define CMD_SEND_BINARY_REQ 50
|
||||
#define CMD_FACTORY_RESET 51
|
||||
#define CMD_SEND_PATH_DISCOVERY_REQ 52
|
||||
|
||||
#define RESP_CODE_OK 0
|
||||
#define RESP_CODE_ERR 1
|
||||
@@ -97,6 +98,7 @@
|
||||
#define PUSH_CODE_NEW_ADVERT 0x8A
|
||||
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
|
||||
#define PUSH_CODE_BINARY_RESPONSE 0x8C
|
||||
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
|
||||
|
||||
#define ERR_CODE_UNSUPPORTED_CMD 1
|
||||
#define ERR_CODE_NOT_FOUND 2
|
||||
@@ -107,10 +109,6 @@
|
||||
|
||||
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
#endif
|
||||
|
||||
void MyMesh::writeOKFrame() {
|
||||
uint8_t buf[1];
|
||||
buf[0] = RESP_CODE_OK;
|
||||
@@ -152,7 +150,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
|
||||
_serial->writeFrame(out_frame, i);
|
||||
}
|
||||
|
||||
void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) {
|
||||
void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) {
|
||||
int i = 0;
|
||||
uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT
|
||||
memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE);
|
||||
@@ -166,23 +164,45 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame,
|
||||
i += 32;
|
||||
memcpy(&contact.last_advert_timestamp, &frame[i], 4);
|
||||
i += 4;
|
||||
if (i + 8 >= len) { // optional fields
|
||||
if (len >= i + 8) { // optional fields
|
||||
memcpy(&contact.gps_lat, &frame[i], 4);
|
||||
i += 4;
|
||||
memcpy(&contact.gps_lon, &frame[i], 4);
|
||||
i += 4;
|
||||
if (len >= i + 4) {
|
||||
memcpy(&last_mod, &frame[i], 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::Frame::isChannelMsg() const {
|
||||
return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3;
|
||||
}
|
||||
|
||||
void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
|
||||
if (offline_queue_len >= OFFLINE_QUEUE_SIZE) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: offline_queue is full!");
|
||||
MESH_DEBUG_PRINTLN("WARN: offline_queue is full!");
|
||||
int pos = 0;
|
||||
while (pos < offline_queue_len) {
|
||||
if (offline_queue[pos].isChannelMsg()) {
|
||||
for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue
|
||||
offline_queue[i] = offline_queue[i + 1];
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue.");
|
||||
offline_queue[offline_queue_len - 1].len = len;
|
||||
memcpy(offline_queue[offline_queue_len - 1].buf, frame, len);
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue.");
|
||||
} else {
|
||||
offline_queue[offline_queue_len].len = len;
|
||||
memcpy(offline_queue[offline_queue_len].buf, frame, len);
|
||||
offline_queue_len++;
|
||||
}
|
||||
}
|
||||
|
||||
int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
|
||||
if (offline_queue_len > 0) { // check offline queue
|
||||
size_t len = offline_queue[0].len; // take from top of queue
|
||||
@@ -210,6 +230,10 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
}
|
||||
|
||||
uint8_t MyMesh::getExtraAckTransmitCount() const {
|
||||
return _prefs.multi_acks;
|
||||
}
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
|
||||
int i = 0;
|
||||
@@ -238,7 +262,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
|
||||
}
|
||||
} else {
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.soundBuzzer(UIEventType::newContactMessage);
|
||||
if (_ui) _ui->notify(UIEventType::newContactMessage);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -258,6 +282,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
|
||||
}
|
||||
|
||||
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
|
||||
strcpy(p->name, contact.name);
|
||||
p->recv_timestamp = getRTCClock()->getCurrentTime();
|
||||
p->path_len = path_len;
|
||||
memcpy(p->path, path, p->path_len);
|
||||
@@ -266,6 +291,20 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
|
||||
static int sort_by_recent(const void *a, const void *b) {
|
||||
return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp;
|
||||
}
|
||||
|
||||
int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
|
||||
if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE;
|
||||
qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent);
|
||||
|
||||
for (int i = 0; i < max_num; i++) {
|
||||
dest[i] = advert_paths[i];
|
||||
}
|
||||
return max_num;
|
||||
}
|
||||
|
||||
void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
|
||||
out_frame[0] = PUSH_CODE_PATH_UPDATED;
|
||||
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
|
||||
@@ -274,7 +313,7 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
|
||||
bool MyMesh::processAck(const uint8_t *data) {
|
||||
ContactInfo* MyMesh::processAck(const uint8_t *data) {
|
||||
// see if matches any in a table
|
||||
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
|
||||
if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient
|
||||
@@ -286,7 +325,7 @@ bool MyMesh::processAck(const uint8_t *data) {
|
||||
|
||||
// NOTE: the same ACK can be received multiple times!
|
||||
expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK
|
||||
return true;
|
||||
return expected_ack_table[i].contact;
|
||||
}
|
||||
}
|
||||
return checkConnectionsAck(data);
|
||||
@@ -330,10 +369,10 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
|
||||
#ifdef DISPLAY_CLASS
|
||||
// we only want to show text messages on display, not cli data
|
||||
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
|
||||
if (should_display) {
|
||||
ui_task.newMsg(path_len, from.name, text, offline_queue_len);
|
||||
if (should_display && _ui) {
|
||||
_ui->newMsg(path_len, from.name, text, offline_queue_len);
|
||||
if (!_serial->isConnected()) {
|
||||
ui_task.soundBuzzer(UIEventType::contactMessage);
|
||||
_ui->notify(UIEventType::contactMessage);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -392,7 +431,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
_serial->writeFrame(frame, 1);
|
||||
} else {
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.soundBuzzer(UIEventType::channelMessage);
|
||||
if (_ui) _ui->notify(UIEventType::channelMessage);
|
||||
#endif
|
||||
}
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -402,7 +441,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
if (getChannel(channel_idx, channel_details)) {
|
||||
channel_name = channel_details.name;
|
||||
}
|
||||
ui_task.newMsg(path_len, channel_name, text, offline_queue_len);
|
||||
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -430,6 +469,9 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
|
||||
permissions |= cp & TELEM_PERM_ENVIRONMENT;
|
||||
}
|
||||
|
||||
uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
|
||||
permissions &= perm_mask;
|
||||
|
||||
if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
@@ -473,6 +515,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
memcpy(&out_frame[i], &tag, 4);
|
||||
i += 4; // NEW: include server timestamp
|
||||
out_frame[i++] = data[7]; // NEW (v7): ACL permissions
|
||||
out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL
|
||||
} else {
|
||||
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
|
||||
out_frame[i++] = 0; // reserved
|
||||
@@ -520,6 +563,39 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
|
||||
uint32_t tag;
|
||||
memcpy(&tag, extra, 4);
|
||||
|
||||
if (tag == pending_discovery) { // check for matching response tag)
|
||||
pending_discovery = 0;
|
||||
|
||||
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
|
||||
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
|
||||
} else {
|
||||
int i = 0;
|
||||
out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE;
|
||||
out_frame[i++] = 0; // reserved
|
||||
memcpy(&out_frame[i], contact.id.pub_key, 6);
|
||||
i += 6; // pub_key_prefix
|
||||
out_frame[i++] = out_path_len;
|
||||
memcpy(&out_frame[i], out_path, out_path_len);
|
||||
i += out_path_len;
|
||||
out_frame[i++] = in_path_len;
|
||||
memcpy(&out_frame[i], in_path, in_path_len);
|
||||
i += in_path_len;
|
||||
// NOTE: telemetry data in 'extra' is discarded at present
|
||||
|
||||
_serial->writeFrame(out_frame, i);
|
||||
}
|
||||
return false; // DON'T send reciprocal path!
|
||||
}
|
||||
}
|
||||
// let base class handle received path and data
|
||||
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
|
||||
}
|
||||
|
||||
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||
if (packet->payload_len + 4 > sizeof(out_frame)) {
|
||||
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
|
||||
@@ -575,14 +651,14 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t
|
||||
|
||||
void MyMesh::onSendTimeout() {}
|
||||
|
||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
|
||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
|
||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store) {
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
|
||||
_iter_started = false;
|
||||
_cli_rescue = false;
|
||||
offline_queue_len = 0;
|
||||
app_target_ver = 0;
|
||||
pending_login = pending_status = pending_telemetry = pending_req = 0;
|
||||
clearPendingReqs();
|
||||
next_ack_idx = 0;
|
||||
sign_data = NULL;
|
||||
dirty_contacts_expiry = 0;
|
||||
@@ -654,6 +730,7 @@ void MyMesh::begin(bool has_display) {
|
||||
_active_ble_pin = 0;
|
||||
#endif
|
||||
|
||||
resetContacts();
|
||||
_store->loadContacts(this);
|
||||
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
|
||||
_store->loadChannels(this);
|
||||
@@ -719,7 +796,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
i += 4;
|
||||
memcpy(&out_frame[i], &lon, 4);
|
||||
i += 4;
|
||||
out_frame[i++] = 0; // reserved
|
||||
out_frame[i++] = _prefs.multi_acks; // new v7+
|
||||
out_frame[i++] = _prefs.advert_loc_policy;
|
||||
out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) |
|
||||
(_prefs.telemetry_mode_base); // v5+
|
||||
@@ -768,6 +845,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (expected_ack) {
|
||||
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table
|
||||
expected_ack_table[next_ack_idx].ack = expected_ack;
|
||||
expected_ack_table[next_ack_idx].contact = recipient;
|
||||
next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
|
||||
}
|
||||
|
||||
@@ -892,15 +970,16 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
} else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) {
|
||||
uint8_t *pub_key = &cmd_frame[1];
|
||||
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
|
||||
if (recipient) {
|
||||
updateContactFromFrame(*recipient, cmd_frame, len);
|
||||
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
|
||||
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
|
||||
recipient->lastmod = last_mod;
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
writeOKFrame();
|
||||
} else {
|
||||
ContactInfo contact;
|
||||
updateContactFromFrame(contact, cmd_frame, len);
|
||||
contact.lastmod = getRTCClock()->getCurrentTime();
|
||||
updateContactFromFrame(contact, last_mod, cmd_frame, len);
|
||||
contact.lastmod = last_mod;
|
||||
contact.sync_since = 0;
|
||||
if (addContact(contact)) {
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
@@ -979,7 +1058,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
|
||||
_serial->writeFrame(out_frame, out_len);
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.msgRead(offline_queue_len);
|
||||
if (_ui) _ui->msgRead(offline_queue_len);
|
||||
#endif
|
||||
} else {
|
||||
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
|
||||
@@ -1050,6 +1129,9 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
|
||||
if (len >= 4) {
|
||||
_prefs.advert_loc_policy = cmd_frame[3];
|
||||
if (len >= 5) {
|
||||
_prefs.multi_acks = cmd_frame[4];
|
||||
}
|
||||
}
|
||||
}
|
||||
savePrefs();
|
||||
@@ -1086,6 +1168,9 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (_store->saveMainIdentity(identity)) {
|
||||
self_id = identity;
|
||||
writeOKFrame();
|
||||
// re-load contacts, to recalc shared secrets
|
||||
resetContacts();
|
||||
_store->loadContacts(this);
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
|
||||
}
|
||||
@@ -1119,7 +1204,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
} else {
|
||||
pending_req = pending_telemetry = pending_status = 0;
|
||||
clearPendingReqs();
|
||||
memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
|
||||
out_frame[0] = RESP_CODE_SENT;
|
||||
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
@@ -1139,7 +1224,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
} else {
|
||||
pending_req = pending_telemetry = pending_login = 0;
|
||||
clearPendingReqs();
|
||||
// FUTURE: pending_status = tag; // match this in onContactResponse()
|
||||
memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
|
||||
out_frame[0] = RESP_CODE_SENT;
|
||||
@@ -1151,6 +1236,35 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) {
|
||||
uint8_t *pub_key = &cmd_frame[2];
|
||||
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (recipient) {
|
||||
uint32_t tag, est_timeout;
|
||||
// 'Path Discovery' is just a special case of flood + Telemetry req
|
||||
uint8_t req_data[9];
|
||||
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
|
||||
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
|
||||
memset(&req_data[2], 0, 3); // reserved
|
||||
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
|
||||
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
|
||||
recipient->out_path_len = -1;
|
||||
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
|
||||
recipient->out_path_len = save;
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
} else {
|
||||
clearPendingReqs();
|
||||
pending_discovery = tag; // match this in onContactResponse()
|
||||
out_frame[0] = RESP_CODE_SENT;
|
||||
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
memcpy(&out_frame[2], &tag, 4);
|
||||
memcpy(&out_frame[6], &est_timeout, 4);
|
||||
_serial->writeFrame(out_frame, 10);
|
||||
}
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ
|
||||
uint8_t *pub_key = &cmd_frame[4];
|
||||
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
@@ -1160,7 +1274,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
} else {
|
||||
pending_status = pending_login = pending_req = 0;
|
||||
clearPendingReqs();
|
||||
pending_telemetry = tag; // match this in onContactResponse()
|
||||
out_frame[0] = RESP_CODE_SENT;
|
||||
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
@@ -1196,7 +1310,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (result == MSG_SEND_FAILED) {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
} else {
|
||||
pending_status = pending_login = pending_telemetry = 0;
|
||||
clearPendingReqs();
|
||||
pending_req = tag; // match this in onContactResponse()
|
||||
out_frame[0] = RESP_CODE_SENT;
|
||||
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
|
||||
@@ -1431,33 +1545,72 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
|
||||
// get path from command e.g: "ls /adafruit"
|
||||
const char *path = &cli_command[3];
|
||||
|
||||
|
||||
bool is_fs2 = false;
|
||||
if (memcmp(path, "UserData/", 9) == 0) {
|
||||
path += 8; // skip "UserData"
|
||||
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
|
||||
path += 7; // skip "ExtraFS"
|
||||
is_fs2 = true;
|
||||
}
|
||||
Serial.printf("Listing files in %s\n", path);
|
||||
|
||||
// log each file and directory
|
||||
File root = _store->openRead(path);
|
||||
if(root){
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
|
||||
if (file.isDirectory()) {
|
||||
Serial.printf("[dir] %s\n", file.name());
|
||||
} else {
|
||||
Serial.printf("[file] %s (%d bytes)\n", file.name(), file.size());
|
||||
if (is_fs2 == false) {
|
||||
if (root) {
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
if (file.isDirectory()) {
|
||||
Serial.printf("[dir] UserData%s/%s\n", path, file.name());
|
||||
} else {
|
||||
Serial.printf("[file] UserData%s/%s (%d bytes)\n", path, file.name(), file.size());
|
||||
}
|
||||
// move to next file
|
||||
file = root.openNextFile();
|
||||
}
|
||||
|
||||
// move to next file
|
||||
file = root.openNextFile();
|
||||
|
||||
root.close();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
|
||||
if (is_fs2 == true || strlen(path) == 0 || strcmp(path, "/") == 0) {
|
||||
if (_store->getSecondaryFS() != nullptr) {
|
||||
File root2 = _store->openRead(_store->getSecondaryFS(), path);
|
||||
File file = root2.openNextFile();
|
||||
while (file) {
|
||||
if (file.isDirectory()) {
|
||||
Serial.printf("[dir] ExtraFS%s/%s\n", path, file.name());
|
||||
} else {
|
||||
Serial.printf("[file] ExtraFS%s/%s (%d bytes)\n", path, file.name(), file.size());
|
||||
}
|
||||
// move to next file
|
||||
file = root2.openNextFile();
|
||||
}
|
||||
root2.close();
|
||||
}
|
||||
}
|
||||
} else if (memcmp(cli_command, "cat", 3) == 0) {
|
||||
|
||||
// get path from command e.g: "cat /contacts3"
|
||||
const char *path = &cli_command[4];
|
||||
|
||||
bool is_fs2 = false;
|
||||
if (memcmp(path, "UserData/", 9) == 0) {
|
||||
path += 8; // skip "UserData"
|
||||
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
|
||||
path += 7; // skip "ExtraFS"
|
||||
is_fs2 = true;
|
||||
} else {
|
||||
Serial.println("Invalid path provided, must start with UserData/ or ExtraFS/");
|
||||
cli_command[0] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// log file content as hex
|
||||
File file = _store->openRead(path);
|
||||
if (is_fs2 == true) {
|
||||
file = _store->openRead(_store->getSecondaryFS(), path);
|
||||
}
|
||||
if(file){
|
||||
|
||||
// get file content
|
||||
@@ -1474,17 +1627,30 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
}
|
||||
|
||||
} else if (memcmp(cli_command, "rm ", 3) == 0) {
|
||||
|
||||
// get path from command e.g: "rm /adv_blobs"
|
||||
const char *path = &cli_command[4];
|
||||
|
||||
const char *path = &cli_command[3];
|
||||
MESH_DEBUG_PRINTLN("Removing file: %s", path);
|
||||
// ensure path is not empty, or root dir
|
||||
if(!path || strlen(path) == 0 || strcmp(path, "/") == 0){
|
||||
Serial.println("Invalid path provided");
|
||||
} else {
|
||||
bool is_fs2 = false;
|
||||
if (memcmp(path, "UserData/", 9) == 0) {
|
||||
path += 8; // skip "UserData"
|
||||
} else if (memcmp(path, "ExtraFS/", 8) == 0) {
|
||||
path += 7; // skip "ExtraFS"
|
||||
is_fs2 = true;
|
||||
}
|
||||
|
||||
// remove file
|
||||
bool removed = _store->removeFile(path);
|
||||
bool removed;
|
||||
if (is_fs2) {
|
||||
MESH_DEBUG_PRINTLN("Removing file from ExtraFS: %s", path);
|
||||
removed = _store->removeFile(_store->getSecondaryFS(), path);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("Removing file from UserData: %s", path);
|
||||
removed = _store->removeFile(path);
|
||||
}
|
||||
if(removed){
|
||||
Serial.println("File removed");
|
||||
} else {
|
||||
@@ -1525,8 +1691,8 @@ void MyMesh::checkSerialInterface() {
|
||||
_serial->writeFrame(out_frame, 5);
|
||||
_iter_started = false;
|
||||
}
|
||||
} else if (!_serial->isWriteBusy()) {
|
||||
checkConnections();
|
||||
//} else if (!_serial->isWriteBusy()) {
|
||||
// checkConnections(); // TODO - deprecate the 'Connections' stuff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1546,7 +1712,7 @@ void MyMesh::loop() {
|
||||
}
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.setHasConnection(_serial->isConnected());
|
||||
if (_ui) _ui->setHasConnection(_serial->isConnected());
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
#endif
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
#define FIRMWARE_VER_CODE 7
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "15 Jul 2025"
|
||||
#define FIRMWARE_BUILD_DATE "28 Sep 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.7.3"
|
||||
#define FIRMWARE_VERSION "v1.9.0"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -77,9 +75,17 @@
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
|
||||
struct AdvertPath {
|
||||
uint8_t pubkey_prefix[7];
|
||||
uint8_t path_len;
|
||||
char name[32];
|
||||
uint32_t recv_timestamp;
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
};
|
||||
|
||||
class MyMesh : public BaseChatMesh, public DataStoreHost {
|
||||
public:
|
||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store);
|
||||
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
|
||||
|
||||
void begin(bool has_display);
|
||||
void startInterface(BaseSerialInterface &serial);
|
||||
@@ -93,16 +99,20 @@ public:
|
||||
bool advert();
|
||||
void enterCLIRescue();
|
||||
|
||||
int getRecentlyHeard(AdvertPath dest[], int max_num);
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
int getInterferenceThreshold() const override;
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
uint8_t getExtraAckTransmitCount() const override;
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
bool isAutoAddEnabled() const override;
|
||||
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||
void onContactPathUpdated(const ContactInfo &contact) override;
|
||||
bool processAck(const uint8_t *data) override;
|
||||
ContactInfo* processAck(const uint8_t *data) override;
|
||||
void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const uint8_t *extra, int extra_len, const char *text);
|
||||
|
||||
@@ -132,12 +142,16 @@ protected:
|
||||
bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); }
|
||||
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
|
||||
|
||||
void clearPendingReqs() {
|
||||
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void writeOKFrame();
|
||||
void writeErrFrame(uint8_t err_code);
|
||||
void writeDisabledFrame();
|
||||
void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
|
||||
void updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len);
|
||||
void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
|
||||
void addToOfflineQueue(const uint8_t frame[], int len);
|
||||
int getFromOfflineQueue(uint8_t frame[]);
|
||||
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
|
||||
@@ -160,9 +174,10 @@ private:
|
||||
NodePrefs _prefs;
|
||||
uint32_t pending_login;
|
||||
uint32_t pending_status;
|
||||
uint32_t pending_telemetry; // pending _TELEMETRY_REQ
|
||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||
uint32_t pending_req; // pending _BINARY_REQ
|
||||
BaseSerialInterface *_serial;
|
||||
AbstractUITask* _ui;
|
||||
|
||||
ContactsIterator _iter;
|
||||
uint32_t _iter_filter_since;
|
||||
@@ -183,6 +198,8 @@ private:
|
||||
struct Frame {
|
||||
uint8_t len;
|
||||
uint8_t buf[MAX_FRAME_SIZE];
|
||||
|
||||
bool isChannelMsg() const;
|
||||
};
|
||||
int offline_queue_len;
|
||||
Frame offline_queue[OFFLINE_QUEUE_SIZE];
|
||||
@@ -190,22 +207,14 @@ private:
|
||||
struct AckTableEntry {
|
||||
unsigned long msg_sent;
|
||||
uint32_t ack;
|
||||
ContactInfo* contact;
|
||||
};
|
||||
#define EXPECTED_ACK_TABLE_SIZE 8
|
||||
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
|
||||
int next_ack_idx;
|
||||
|
||||
struct AdvertPath {
|
||||
uint8_t pubkey_prefix[7];
|
||||
uint8_t path_len;
|
||||
uint32_t recv_timestamp;
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
};
|
||||
#define ADVERT_PATH_TABLE_SIZE 16
|
||||
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
|
||||
};
|
||||
|
||||
extern MyMesh the_mesh;
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern UITask ui_task;
|
||||
#endif
|
||||
@@ -14,7 +14,7 @@ struct NodePrefs { // persisted to file
|
||||
float freq;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t reserved1;
|
||||
uint8_t multi_acks;
|
||||
uint8_t manual_add_contacts;
|
||||
float bw;
|
||||
uint8_t tx_power_dbm;
|
||||
|
||||
@@ -14,7 +14,18 @@ static uint32_t _atoi(const char* sp) {
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
DataStore store(InternalFS, rtc_clock);
|
||||
#if defined(QSPIFLASH)
|
||||
#include <CustomLFS_QSPIFlash.h>
|
||||
DataStore store(InternalFS, QSPIFlash, rtc_clock);
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
#include <CustomLFS.h>
|
||||
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
|
||||
DataStore store(InternalFS, ExtraFS, rtc_clock);
|
||||
#else
|
||||
DataStore store(InternalFS, rtc_clock);
|
||||
#endif
|
||||
#endif
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
DataStore store(LittleFS, rtc_clock);
|
||||
@@ -75,14 +86,19 @@ static uint32_t _atoi(const char* sp) {
|
||||
#endif
|
||||
|
||||
/* GLOBAL OBJECTS */
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
UITask ui_task(&board);
|
||||
UITask ui_task(&board, &serial_interface);
|
||||
#endif
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
#ifdef DISPLAY_CLASS
|
||||
, &ui_task
|
||||
#endif
|
||||
);
|
||||
|
||||
/* END GLOBAL OBJECTS */
|
||||
|
||||
void halt() {
|
||||
@@ -99,7 +115,10 @@ void setup() {
|
||||
if (display.begin()) {
|
||||
disp = &display;
|
||||
disp->startFrame();
|
||||
disp->print("Please wait...");
|
||||
#ifdef ST7789
|
||||
disp->setTextSize(2);
|
||||
#endif
|
||||
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
|
||||
disp->endFrame();
|
||||
}
|
||||
#endif
|
||||
@@ -110,6 +129,18 @@ void setup() {
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
InternalFS.begin();
|
||||
#if defined(QSPIFLASH)
|
||||
if (!QSPIFlash.begin()) {
|
||||
// debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here?
|
||||
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: failed to initialize");
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: initialized successfully");
|
||||
}
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
ExtraFS.begin();
|
||||
#endif
|
||||
#endif
|
||||
store.begin();
|
||||
the_mesh.begin(
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
811
examples/companion_radio/ui-new/UITask.cpp
Normal file
811
examples/companion_radio/ui-new/UITask.cpp
Normal file
@@ -0,0 +1,811 @@
|
||||
#include "UITask.h"
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "target.h"
|
||||
|
||||
#ifndef AUTO_OFF_MILLIS
|
||||
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
#endif
|
||||
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
|
||||
|
||||
#ifdef PIN_STATUS_LED
|
||||
#define LED_ON_MILLIS 20
|
||||
#define LED_ON_MSG_MILLIS 200
|
||||
#define LED_CYCLE_MILLIS 4000
|
||||
#endif
|
||||
|
||||
#define LONG_PRESS_MILLIS 1200
|
||||
|
||||
#ifndef UI_RECENT_LIST_SIZE
|
||||
#define UI_RECENT_LIST_SIZE 4
|
||||
#endif
|
||||
|
||||
#define PRESS_LABEL "long press"
|
||||
|
||||
#include "icons.h"
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
unsigned long dismiss_after;
|
||||
char _version_info[12];
|
||||
|
||||
public:
|
||||
SplashScreen(UITask* task) : _task(task) {
|
||||
// strip off dash and commit hash by changing dash to null terminator
|
||||
// e.g: v1.2.3-abcdef -> v1.2.3
|
||||
const char *ver = FIRMWARE_VERSION;
|
||||
const char *dash = strchr(ver, '-');
|
||||
|
||||
int len = dash ? dash - ver : strlen(ver);
|
||||
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1;
|
||||
memcpy(_version_info, ver, len);
|
||||
_version_info[len] = 0;
|
||||
|
||||
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
// meshcore logo
|
||||
display.setColor(DisplayDriver::BLUE);
|
||||
int logoWidth = 128;
|
||||
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// version info
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width()/2, 22, _version_info);
|
||||
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
|
||||
|
||||
return 1000;
|
||||
}
|
||||
|
||||
void poll() override {
|
||||
if (millis() >= dismiss_after) {
|
||||
_task->gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class HomeScreen : public UIScreen {
|
||||
enum HomePage {
|
||||
FIRST,
|
||||
RECENT,
|
||||
RADIO,
|
||||
BLUETOOTH,
|
||||
ADVERT,
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
SENSORS,
|
||||
#endif
|
||||
SHUTDOWN,
|
||||
Count // keep as last
|
||||
};
|
||||
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
SensorManager* _sensors;
|
||||
NodePrefs* _node_prefs;
|
||||
uint8_t _page;
|
||||
bool _shutdown_init;
|
||||
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
||||
|
||||
|
||||
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
||||
// Convert millivolts to percentage
|
||||
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
|
||||
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
|
||||
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
|
||||
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
|
||||
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
|
||||
|
||||
// battery icon
|
||||
int iconWidth = 24;
|
||||
int iconHeight = 10;
|
||||
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
|
||||
int iconY = 0;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
|
||||
// battery outline
|
||||
display.drawRect(iconX, iconY, iconWidth, iconHeight);
|
||||
|
||||
// battery "cap"
|
||||
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
|
||||
|
||||
// fill the battery based on the percentage
|
||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||
}
|
||||
|
||||
CayenneLPP sensors_lpp;
|
||||
int sensors_nb = 0;
|
||||
bool sensors_scroll = false;
|
||||
int sensors_scroll_offset = 0;
|
||||
int next_sensors_refresh = 0;
|
||||
|
||||
void refresh_sensors() {
|
||||
if (millis() > next_sensors_refresh) {
|
||||
sensors_lpp.reset();
|
||||
sensors_nb = 0;
|
||||
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
sensors.querySensors(0xFF, sensors_lpp);
|
||||
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
||||
uint8_t channel, type;
|
||||
while(reader.readHeader(channel, type)) {
|
||||
reader.skipData(type);
|
||||
sensors_nb ++;
|
||||
}
|
||||
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
||||
#else
|
||||
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
|
||||
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
||||
_shutdown_init(false), sensors_lpp(200) { }
|
||||
|
||||
void poll() override {
|
||||
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
||||
_task->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
// node name
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
display.setCursor(0, 0);
|
||||
display.print(filtered_name);
|
||||
|
||||
// battery voltage
|
||||
renderBatteryIndicator(display, _task->getBattMilliVolts());
|
||||
|
||||
// curr page indicator
|
||||
int y = 14;
|
||||
int x = display.width() / 2 - 25;
|
||||
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
|
||||
if (i == _page) {
|
||||
display.fillRect(x-1, y-1, 3, 3);
|
||||
} else {
|
||||
display.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (_page == HomePage::FIRST) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||
display.drawTextCentered(display.width() / 2, 20, tmp);
|
||||
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
|
||||
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
||||
display.setColor(DisplayDriver::RED);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
||||
display.drawTextCentered(display.width() / 2, 43, tmp);
|
||||
}
|
||||
} else if (_page == HomePage::RECENT) {
|
||||
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
int y = 20;
|
||||
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
|
||||
auto a = &recent[i];
|
||||
if (a->name[0] == 0) continue; // empty slot
|
||||
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
|
||||
if (secs < 60) {
|
||||
sprintf(tmp, "%ds", secs);
|
||||
} else if (secs < 60*60) {
|
||||
sprintf(tmp, "%dm", secs / 60);
|
||||
} else {
|
||||
sprintf(tmp, "%dh", secs / (60*60));
|
||||
}
|
||||
|
||||
int timestamp_width = display.getTextWidth(tmp);
|
||||
int max_name_width = display.width() - timestamp_width - 1;
|
||||
|
||||
char filtered_recent_name[sizeof(a->name)];
|
||||
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
|
||||
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
|
||||
display.setCursor(display.width() - timestamp_width - 1, y);
|
||||
display.print(tmp);
|
||||
}
|
||||
} else if (_page == HomePage::RADIO) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(1);
|
||||
// freq / sf
|
||||
display.setCursor(0, 20);
|
||||
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
|
||||
display.print(tmp);
|
||||
|
||||
display.setCursor(0, 31);
|
||||
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
||||
display.print(tmp);
|
||||
|
||||
// tx power, noise floor
|
||||
display.setCursor(0, 42);
|
||||
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm);
|
||||
display.print(tmp);
|
||||
display.setCursor(0, 53);
|
||||
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
|
||||
display.print(tmp);
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 18,
|
||||
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
|
||||
32, 32);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
|
||||
} else if (_page == HomePage::ADVERT) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
|
||||
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
} else if (_page == HomePage::SENSORS) {
|
||||
int y = 18;
|
||||
refresh_sensors();
|
||||
char buf[30];
|
||||
char name[30];
|
||||
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
||||
|
||||
for (int i = 0; i < sensors_scroll_offset; i++) {
|
||||
uint8_t channel, type;
|
||||
r.readHeader(channel, type);
|
||||
r.skipData(type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) {
|
||||
uint8_t channel, type;
|
||||
if (!r.readHeader(channel, type)) { // reached end, reset
|
||||
r.reset();
|
||||
r.readHeader(channel, type);
|
||||
}
|
||||
|
||||
display.setCursor(0, y);
|
||||
float v;
|
||||
switch (type) {
|
||||
case LPP_GPS: // GPS
|
||||
float lat, lon, alt;
|
||||
r.readGPS(lat, lon, alt);
|
||||
strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon);
|
||||
break;
|
||||
case LPP_VOLTAGE:
|
||||
r.readVoltage(v);
|
||||
strcpy(name, "voltage"); sprintf(buf, "%6.2f", v);
|
||||
break;
|
||||
case LPP_CURRENT:
|
||||
r.readCurrent(v);
|
||||
strcpy(name, "current"); sprintf(buf, "%.3f", v);
|
||||
break;
|
||||
case LPP_TEMPERATURE:
|
||||
r.readTemperature(v);
|
||||
strcpy(name, "temperature"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
r.readRelativeHumidity(v);
|
||||
strcpy(name, "humidity"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
r.readPressure(v);
|
||||
strcpy(name, "pressure"); sprintf(buf, "%.2f", v);
|
||||
break;
|
||||
case LPP_ALTITUDE:
|
||||
r.readAltitude(v);
|
||||
strcpy(name, "altitude"); sprintf(buf, "%.0f", v);
|
||||
break;
|
||||
case LPP_POWER:
|
||||
r.readPower(v);
|
||||
strcpy(name, "power"); sprintf(buf, "%6.2f", v);
|
||||
break;
|
||||
default:
|
||||
r.skipData(type);
|
||||
strcpy(name, "unk"); sprintf(buf, "");
|
||||
}
|
||||
display.setCursor(0, y);
|
||||
display.print(name);
|
||||
display.setCursor(
|
||||
display.width()-display.getTextWidth(buf)-1, y
|
||||
);
|
||||
display.print(buf);
|
||||
y = y + 12;
|
||||
}
|
||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||
else sensors_scroll_offset = 0;
|
||||
#endif
|
||||
} else if (_page == HomePage::SHUTDOWN) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
if (_shutdown_init) {
|
||||
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
|
||||
} else {
|
||||
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
|
||||
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate: " PRESS_LABEL);
|
||||
}
|
||||
}
|
||||
return 5000; // next render after 5000 ms
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_LEFT || c == KEY_PREV) {
|
||||
_page = (_page + HomePage::Count - 1) % HomePage::Count;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_NEXT || c == KEY_RIGHT) {
|
||||
_page = (_page + 1) % HomePage::Count;
|
||||
if (_page == HomePage::RECENT) {
|
||||
_task->showAlert("Recent adverts", 800);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
||||
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
||||
_task->disableSerial();
|
||||
} else {
|
||||
_task->enableSerial();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
|
||||
_task->notify(UIEventType::ack);
|
||||
if (the_mesh.advert()) {
|
||||
_task->showAlert("Advert sent!", 1000);
|
||||
} else {
|
||||
_task->showAlert("Advert failed..", 1000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
|
||||
_task->toggleGPS();
|
||||
next_sensors_refresh=0;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
||||
_shutdown_init = true; // need to wait for button to be released
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class MsgPreviewScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
|
||||
struct MsgEntry {
|
||||
uint32_t timestamp;
|
||||
char origin[62];
|
||||
char msg[78];
|
||||
};
|
||||
#define MAX_UNREAD_MSGS 32
|
||||
int num_unread;
|
||||
MsgEntry unread[MAX_UNREAD_MSGS];
|
||||
|
||||
public:
|
||||
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
|
||||
|
||||
void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
|
||||
if (num_unread >= MAX_UNREAD_MSGS) return; // full
|
||||
|
||||
auto p = &unread[num_unread++];
|
||||
p->timestamp = _rtc->getCurrentTime();
|
||||
if (path_len == 0xFF) {
|
||||
sprintf(p->origin, "(D) %s:", from_name);
|
||||
} else {
|
||||
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name);
|
||||
}
|
||||
StrHelper::strncpy(p->msg, msg, sizeof(p->msg));
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[16];
|
||||
display.setCursor(0, 0);
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
sprintf(tmp, "Unread: %d", num_unread);
|
||||
display.print(tmp);
|
||||
|
||||
auto p = &unread[0];
|
||||
|
||||
int secs = _rtc->getCurrentTime() - p->timestamp;
|
||||
if (secs < 60) {
|
||||
sprintf(tmp, "%ds", secs);
|
||||
} else if (secs < 60*60) {
|
||||
sprintf(tmp, "%dm", secs / 60);
|
||||
} else {
|
||||
sprintf(tmp, "%dh", secs / (60*60));
|
||||
}
|
||||
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
|
||||
display.print(tmp);
|
||||
|
||||
display.drawRect(0, 11, display.width(), 1); // horiz line
|
||||
|
||||
display.setCursor(0, 14);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
char filtered_origin[sizeof(p->origin)];
|
||||
display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin));
|
||||
display.print(filtered_origin);
|
||||
|
||||
display.setCursor(0, 25);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char filtered_msg[sizeof(p->msg)];
|
||||
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
|
||||
display.printWordWrap(filtered_msg, display.width());
|
||||
|
||||
#if AUTO_OFF_MILLIS==0 // probably e-ink
|
||||
return 10000; // 10 s
|
||||
#else
|
||||
return 1000; // next render after 1000 ms
|
||||
#endif
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_NEXT || c == KEY_RIGHT) {
|
||||
num_unread--;
|
||||
if (num_unread == 0) {
|
||||
_task->gotoHomeScreen();
|
||||
} else {
|
||||
// delete first/curr item from unread queue
|
||||
for (int i = 0; i < num_unread; i++) {
|
||||
unread[i] = unread[i + 1];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER) {
|
||||
num_unread = 0; // clear unread queue
|
||||
_task->gotoHomeScreen();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
user_btn.begin();
|
||||
#endif
|
||||
#if defined(PIN_USER_BTN_ANA)
|
||||
analog_btn.begin();
|
||||
#endif
|
||||
|
||||
_node_prefs = node_prefs;
|
||||
if (_display != NULL) {
|
||||
_display->turnOn();
|
||||
}
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.begin();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
vibration.begin();
|
||||
#endif
|
||||
|
||||
ui_started_at = millis();
|
||||
_alert_expiry = 0;
|
||||
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
|
||||
void UITask::showAlert(const char* text, int duration_millis) {
|
||||
strcpy(_alert, text);
|
||||
_alert_expiry = millis() + duration_millis;
|
||||
}
|
||||
|
||||
void UITask::notify(UIEventType t) {
|
||||
#if defined(PIN_BUZZER)
|
||||
switch(t){
|
||||
case UIEventType::contactMessage:
|
||||
// gemini's pick
|
||||
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
|
||||
break;
|
||||
case UIEventType::channelMessage:
|
||||
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
|
||||
break;
|
||||
case UIEventType::ack:
|
||||
buzzer.play("ack:d=32,o=8,b=120:c");
|
||||
break;
|
||||
case UIEventType::roomMessage:
|
||||
case UIEventType::newContactMessage:
|
||||
case UIEventType::none:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
// Trigger vibration for all UI events except none
|
||||
if (t != UIEventType::none) {
|
||||
vibration.trigger();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
|
||||
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
|
||||
setCurrScreen(msg_preview);
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::userLedHandler() {
|
||||
#ifdef PIN_STATUS_LED
|
||||
int cur_time = millis();
|
||||
if (cur_time > next_led_change) {
|
||||
if (led_state == 0) {
|
||||
led_state = 1;
|
||||
if (_msgcount > 0) {
|
||||
last_led_increment = LED_ON_MSG_MILLIS;
|
||||
} else {
|
||||
last_led_increment = LED_ON_MILLIS;
|
||||
}
|
||||
next_led_change = cur_time + last_led_increment;
|
||||
} else {
|
||||
led_state = 0;
|
||||
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
|
||||
}
|
||||
digitalWrite(PIN_STATUS_LED, led_state);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::setCurrScreen(UIScreen* c) {
|
||||
curr = c;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
/*
|
||||
hardware-agnostic pre-shutdown activity should be done here
|
||||
*/
|
||||
void UITask::shutdown(bool restart){
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
/* note: we have a choice here -
|
||||
we can do a blocking buzzer.loop() with non-deterministic consequences
|
||||
or we can set a flag and delay the shutdown for a couple of seconds
|
||||
while a non-blocking buzzer.loop() plays out in UITask::loop()
|
||||
*/
|
||||
buzzer.shutdown();
|
||||
uint32_t buzzer_timer = millis(); // fail-safe shutdown
|
||||
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
|
||||
buzzer.loop();
|
||||
|
||||
#endif // PIN_BUZZER
|
||||
|
||||
if (restart) {
|
||||
_board->reboot();
|
||||
} else {
|
||||
_display->turnOff();
|
||||
_board->powerOff();
|
||||
}
|
||||
}
|
||||
|
||||
bool UITask::isButtonPressed() const {
|
||||
#ifdef PIN_USER_BTN
|
||||
return user_btn.isPressed();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
char c = 0;
|
||||
#if defined(PIN_USER_BTN)
|
||||
int ev = user_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_PREV);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#endif
|
||||
#if defined(WIO_TRACKER_L1)
|
||||
ev = joystick_left.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_LEFT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_LEFT);
|
||||
}
|
||||
ev = joystick_right.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_RIGHT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_RIGHT);
|
||||
}
|
||||
#endif
|
||||
#if defined(PIN_USER_BTN_ANA)
|
||||
ev = analog_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_PREV);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#endif
|
||||
#if defined(DISP_BACKLIGHT) && defined(BACKLIGHT_BTN)
|
||||
if (millis() > next_backlight_btn_check) {
|
||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||
digitalWrite(DISP_BACKLIGHT, !touch_state);
|
||||
next_backlight_btn_check = millis() + 300;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (c != 0 && curr) {
|
||||
curr->handleInput(c);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
|
||||
userLedHandler();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isPlaying()) buzzer.loop();
|
||||
#endif
|
||||
|
||||
if (curr) curr->poll();
|
||||
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
if (millis() >= _next_refresh && curr) {
|
||||
_display->startFrame();
|
||||
int delay_millis = curr->render(*_display);
|
||||
if (millis() < _alert_expiry) { // render alert popup
|
||||
_display->setTextSize(1);
|
||||
int y = _display->height() / 3;
|
||||
int p = _display->height() / 32;
|
||||
_display->setColor(DisplayDriver::DARK);
|
||||
_display->fillRect(p, y, _display->width() - p*2, y);
|
||||
_display->setColor(DisplayDriver::LIGHT); // draw box border
|
||||
_display->drawRect(p, y, _display->width() - p*2, y);
|
||||
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
|
||||
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
|
||||
} else {
|
||||
_next_refresh = millis() + delay_millis;
|
||||
}
|
||||
_display->endFrame();
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
if (millis() > _auto_off) {
|
||||
_display->turnOff();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
vibration.loop();
|
||||
#endif
|
||||
|
||||
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
|
||||
if (millis() > next_batt_chck) {
|
||||
uint16_t milliVolts = getBattMilliVolts();
|
||||
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
|
||||
|
||||
// show low battery shutdown alert
|
||||
// we should only do this for eink displays, which will persist after power loss
|
||||
#if defined(THINKNODE_M1) || defined(LILYGO_TECHO)
|
||||
if (_display != NULL) {
|
||||
_display->startFrame();
|
||||
_display->setTextSize(2);
|
||||
_display->setColor(DisplayDriver::RED);
|
||||
_display->drawTextCentered(_display->width() / 2, 20, "Low Battery.");
|
||||
_display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!");
|
||||
_display->endFrame();
|
||||
}
|
||||
#endif
|
||||
|
||||
shutdown();
|
||||
|
||||
}
|
||||
next_batt_chck = millis() + 8000;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
char UITask::checkDisplayOn(char c) {
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn(); // turn display on and consume event
|
||||
c = 0;
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 0; // trigger refresh
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleLongPress(char c) {
|
||||
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
||||
the_mesh.enterCLIRescue();
|
||||
c = 0; // consume event
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleDoubleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: double click triggered");
|
||||
checkDisplayOn(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleTripleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
|
||||
checkDisplayOn(c);
|
||||
toggleBuzzer();
|
||||
c = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
void UITask::toggleGPS() {
|
||||
if (_sensors != NULL) {
|
||||
// toggle GPS on/off
|
||||
int num = _sensors->getNumSettings();
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
||||
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
|
||||
_sensors->setSettingValue("gps", "0");
|
||||
notify(UIEventType::ack);
|
||||
showAlert("GPS: Disabled", 800);
|
||||
} else {
|
||||
_sensors->setSettingValue("gps", "1");
|
||||
notify(UIEventType::ack);
|
||||
showAlert("GPS: Enabled", 800);
|
||||
}
|
||||
_next_refresh = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::toggleBuzzer() {
|
||||
// Toggle buzzer quiet mode
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isQuiet()) {
|
||||
buzzer.quiet(false);
|
||||
notify(UIEventType::ack);
|
||||
showAlert("Buzzer: ON", 800);
|
||||
} else {
|
||||
buzzer.quiet(true);
|
||||
showAlert("Buzzer: OFF", 800);
|
||||
}
|
||||
_next_refresh = 0; // trigger refresh
|
||||
#endif
|
||||
}
|
||||
84
examples/companion_radio/ui-new/UITask.h
Normal file
84
examples/companion_radio/ui-new/UITask.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/BaseSerialInterface.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
#include <helpers/ui/buzzer.h>
|
||||
#endif
|
||||
#ifdef PIN_VIBRATION
|
||||
#include <helpers/ui/GenericVibration.h>
|
||||
#endif
|
||||
|
||||
#include "../AbstractUITask.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
SensorManager* _sensors;
|
||||
#ifdef PIN_BUZZER
|
||||
genericBuzzer buzzer;
|
||||
#endif
|
||||
#ifdef PIN_VIBRATION
|
||||
GenericVibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
NodePrefs* _node_prefs;
|
||||
char _alert[80];
|
||||
unsigned long _alert_expiry;
|
||||
int _msgcount;
|
||||
unsigned long ui_started_at, next_batt_chck;
|
||||
int next_backlight_btn_check = 0;
|
||||
#ifdef PIN_STATUS_LED
|
||||
int led_state = 0;
|
||||
int next_led_change = 0;
|
||||
int last_led_increment = 0;
|
||||
#endif
|
||||
|
||||
UIScreen* splash;
|
||||
UIScreen* home;
|
||||
UIScreen* msg_preview;
|
||||
UIScreen* curr;
|
||||
|
||||
void userLedHandler();
|
||||
|
||||
// Button action handlers
|
||||
char checkDisplayOn(char c);
|
||||
char handleLongPress(char c);
|
||||
char handleDoubleClick(char c);
|
||||
char handleTripleClick(char c);
|
||||
|
||||
void setCurrScreen(UIScreen* c);
|
||||
|
||||
public:
|
||||
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
|
||||
next_batt_chck = _next_refresh = 0;
|
||||
ui_started_at = 0;
|
||||
curr = NULL;
|
||||
}
|
||||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
||||
|
||||
void gotoHomeScreen() { setCurrScreen(home); }
|
||||
void showAlert(const char* text, int duration_millis);
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
|
||||
void toggleBuzzer();
|
||||
void toggleGPS();
|
||||
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
||||
void notify(UIEventType t = UIEventType::none) override;
|
||||
void loop() override;
|
||||
|
||||
void shutdown(bool restart = false);
|
||||
};
|
||||
118
examples/companion_radio/ui-new/icons.h
Normal file
118
examples/companion_radio/ui-new/icons.h
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// 'meshcore', 128x13px
|
||||
static const uint8_t meshcore_logo [] = {
|
||||
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
|
||||
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
|
||||
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
|
||||
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
|
||||
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
|
||||
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
|
||||
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
|
||||
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
|
||||
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
};
|
||||
|
||||
static const uint8_t bluetooth_on[] = {
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x30, 0x00, 0x00,
|
||||
0x00, 0x3C, 0x00, 0x00,
|
||||
0x00, 0x3E, 0x00, 0x00,
|
||||
0x00, 0x3F, 0x80, 0x00,
|
||||
0x00, 0x3F, 0xC0, 0x00,
|
||||
0x00, 0x3B, 0xE0, 0x00,
|
||||
0x30, 0x38, 0xF8, 0x00,
|
||||
0x3C, 0x38, 0x7C, 0x00,
|
||||
0x3E, 0x38, 0x7C, 0x00,
|
||||
0x1F, 0xB8, 0xF8, 0x70,
|
||||
0x07, 0xF9, 0xF0, 0x78,
|
||||
0x03, 0xFF, 0xC0, 0x78,
|
||||
0x00, 0xFF, 0x80, 0x3C,
|
||||
0x00, 0x7F, 0x07, 0x1C,
|
||||
0x00, 0x7E, 0x07, 0x1C,
|
||||
0x03, 0xFF, 0x82, 0x1C,
|
||||
0x03, 0xFF, 0xC0, 0x78,
|
||||
0x07, 0xFB, 0xE0, 0x78,
|
||||
0x0F, 0xB8, 0xF8, 0x70,
|
||||
0x3E, 0x38, 0x7C, 0x00,
|
||||
0x3C, 0x38, 0x7C, 0x00,
|
||||
0x38, 0x38, 0xF8, 0x00,
|
||||
0x00, 0x39, 0xF0, 0x00,
|
||||
0x00, 0x3F, 0xC0, 0x00,
|
||||
0x00, 0x3F, 0x80, 0x00,
|
||||
0x00, 0x3E, 0x00, 0x00,
|
||||
0x00, 0x3C, 0x00, 0x00,
|
||||
0x00, 0x38, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t bluetooth_off[] = {
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x03, 0x80, 0x00,
|
||||
0x00, 0x03, 0xC0, 0x00,
|
||||
0x00, 0x03, 0xE0, 0x00,
|
||||
0x38, 0x03, 0xF8, 0x00,
|
||||
0x3C, 0x03, 0xFC, 0x00,
|
||||
0x3E, 0x03, 0xBF, 0x00,
|
||||
0x0F, 0x83, 0x8F, 0x80,
|
||||
0x07, 0xC3, 0x87, 0xC0,
|
||||
0x03, 0xF0, 0x03, 0xC0,
|
||||
0x00, 0xF8, 0x0F, 0x80,
|
||||
0x00, 0x7C, 0x0F, 0x00,
|
||||
0x00, 0x1F, 0x0E, 0x00,
|
||||
0x00, 0x0F, 0x80, 0x00,
|
||||
0x00, 0x07, 0xE0, 0x00,
|
||||
0x00, 0x07, 0xF0, 0x00,
|
||||
0x00, 0x0F, 0xF8, 0x00,
|
||||
0x00, 0x3F, 0xBE, 0x00,
|
||||
0x00, 0x7F, 0x9F, 0x00,
|
||||
0x00, 0xFB, 0x8F, 0xC0,
|
||||
0x03, 0xE3, 0x83, 0xE0,
|
||||
0x03, 0xC3, 0x87, 0xF0,
|
||||
0x03, 0x83, 0x8F, 0xFC,
|
||||
0x00, 0x03, 0xBF, 0x3C,
|
||||
0x00, 0x03, 0xFC, 0x1C,
|
||||
0x00, 0x03, 0xF8, 0x00,
|
||||
0x00, 0x03, 0xE0, 0x00,
|
||||
0x00, 0x03, 0xC0, 0x00,
|
||||
0x00, 0x03, 0x80, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t power_icon[] = {
|
||||
0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00,
|
||||
0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80,
|
||||
0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0,
|
||||
0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC,
|
||||
0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E,
|
||||
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E,
|
||||
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C,
|
||||
0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8,
|
||||
0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0,
|
||||
0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00,
|
||||
0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t advert_icon[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30,
|
||||
0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C,
|
||||
0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86,
|
||||
0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
|
||||
0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
|
||||
0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86,
|
||||
0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C,
|
||||
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
|
||||
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "UITask.h"
|
||||
#include <Arduino.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "NodePrefs.h"
|
||||
#include "MyMesh.h"
|
||||
#include "../MyMesh.h"
|
||||
|
||||
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
|
||||
@@ -89,9 +88,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
ui_started_at = millis();
|
||||
}
|
||||
|
||||
void UITask::soundBuzzer(UIEventType bet) {
|
||||
void UITask::notify(UIEventType t) {
|
||||
#if defined(PIN_BUZZER)
|
||||
switch(bet){
|
||||
switch(t){
|
||||
case UIEventType::contactMessage:
|
||||
// gemini's pick
|
||||
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
|
||||
@@ -109,8 +108,8 @@ switch(bet){
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
// Serial.print("DBG: Buzzzzzz -> ");
|
||||
// Serial.println((int) bet);
|
||||
// Serial.print("DBG: Alert user -> ");
|
||||
// Serial.println((int) t);
|
||||
}
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
@@ -371,7 +370,7 @@ void UITask::handleButtonDoublePress() {
|
||||
MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert");
|
||||
// ADVERT
|
||||
#ifdef PIN_BUZZER
|
||||
soundBuzzer(UIEventType::ack);
|
||||
notify(UIEventType::ack);
|
||||
#endif
|
||||
if (the_mesh.advert()) {
|
||||
MESH_DEBUG_PRINTLN("Advert sent!");
|
||||
@@ -389,7 +388,7 @@ void UITask::handleButtonTriplePress() {
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isQuiet()) {
|
||||
buzzer.quiet(false);
|
||||
soundBuzzer(UIEventType::ack);
|
||||
notify(UIEventType::ack);
|
||||
sprintf(_alert, "Buzzer: ON");
|
||||
} else {
|
||||
buzzer.quiet(true);
|
||||
@@ -408,8 +407,12 @@ void UITask::handleButtonQuadruplePress() {
|
||||
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
||||
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
|
||||
_sensors->setSettingValue("gps", "0");
|
||||
notify(UIEventType::ack);
|
||||
sprintf(_alert, "GPS: Disabled");
|
||||
} else {
|
||||
_sensors->setSettingValue("gps", "1");
|
||||
notify(UIEventType::ack);
|
||||
sprintf(_alert, "GPS: Enabled");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -9,28 +9,18 @@
|
||||
#include <helpers/ui/buzzer.h>
|
||||
#endif
|
||||
|
||||
#include "NodePrefs.h"
|
||||
#include "../AbstractUITask.h"
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
#include "Button.h"
|
||||
|
||||
enum class UIEventType
|
||||
{
|
||||
none,
|
||||
contactMessage,
|
||||
channelMessage,
|
||||
roomMessage,
|
||||
newContactMessage,
|
||||
ack
|
||||
};
|
||||
|
||||
class UITask {
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
mesh::MainBoard* _board;
|
||||
SensorManager* _sensors;
|
||||
#ifdef PIN_BUZZER
|
||||
genericBuzzer buzzer;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
bool _connected;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
char _origin[62];
|
||||
@@ -64,19 +54,20 @@ class UITask {
|
||||
|
||||
public:
|
||||
|
||||
UITask(mesh::MainBoard* board) : _board(board), _display(NULL), _sensors(NULL) {
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
|
||||
_next_refresh = 0;
|
||||
ui_started_at = 0;
|
||||
_connected = false;
|
||||
}
|
||||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
|
||||
|
||||
void setHasConnection(bool connected) { _connected = connected; }
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
void clearMsgPreview();
|
||||
void msgRead(int msgcount);
|
||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount);
|
||||
void soundBuzzer(UIEventType bet = UIEventType::none);
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
|
||||
void notify(UIEventType t = UIEventType::none) override;
|
||||
void loop() override;
|
||||
|
||||
void shutdown(bool restart = false);
|
||||
void loop();
|
||||
};
|
||||
866
examples/simple_repeater/MyMesh.cpp
Normal file
866
examples/simple_repeater/MyMesh.cpp
Normal file
@@ -0,0 +1,866 @@
|
||||
#include "MyMesh.h"
|
||||
#include <algorithm>
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
#define LORA_FREQ 915.0
|
||||
#endif
|
||||
#ifndef LORA_BW
|
||||
#define LORA_BW 250
|
||||
#endif
|
||||
#ifndef LORA_SF
|
||||
#define LORA_SF 10
|
||||
#endif
|
||||
#ifndef LORA_CR
|
||||
#define LORA_CR 5
|
||||
#endif
|
||||
#ifndef LORA_TX_POWER
|
||||
#define LORA_TX_POWER 20
|
||||
#endif
|
||||
|
||||
#ifndef ADVERT_NAME
|
||||
#define ADVERT_NAME "repeater"
|
||||
#endif
|
||||
#ifndef ADVERT_LAT
|
||||
#define ADVERT_LAT 0.0
|
||||
#endif
|
||||
#ifndef ADVERT_LON
|
||||
#define ADVERT_LON 0.0
|
||||
#endif
|
||||
|
||||
#ifndef ADMIN_PASSWORD
|
||||
#define ADMIN_PASSWORD "password"
|
||||
#endif
|
||||
|
||||
#ifndef SERVER_RESPONSE_DELAY
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
#endif
|
||||
|
||||
#ifndef TXT_ACK_DELAY
|
||||
#define TXT_ACK_DELAY 200
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_VER_LEVEL 1
|
||||
|
||||
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
||||
#define REQ_TYPE_GET_NEIGHBOURS 0x06
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
#define CLI_REPLY_DELAY_MILLIS 600
|
||||
|
||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||
|
||||
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) {
|
||||
#if MAX_NEIGHBOURS // check if neighbours enabled
|
||||
// find existing neighbour, else use least recently updated
|
||||
uint32_t oldest_timestamp = 0xFFFFFFFF;
|
||||
NeighbourInfo *neighbour = &neighbours[0];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
// if neighbour already known, we should update it
|
||||
if (id.matches(neighbours[i].id)) {
|
||||
neighbour = &neighbours[i];
|
||||
break;
|
||||
}
|
||||
|
||||
// otherwise we should update the least recently updated neighbour
|
||||
if (neighbours[i].heard_timestamp < oldest_timestamp) {
|
||||
neighbour = &neighbours[i];
|
||||
oldest_timestamp = neighbour->heard_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// update neighbour info
|
||||
neighbour->id = id;
|
||||
neighbour->advert_timestamp = timestamp;
|
||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
neighbour->snr = (int8_t)(snr * 4);
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
|
||||
ClientInfo* client = NULL;
|
||||
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
||||
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
|
||||
if (client == NULL) {
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client == NULL) {
|
||||
uint8_t perms;
|
||||
if (strcmp((char *)data, _prefs.password) == 0) { // check for valid admin password
|
||||
perms = PERM_ACL_ADMIN;
|
||||
} else if (strcmp((char *)data, _prefs.guest_password) == 0) { // check guest password
|
||||
perms = PERM_ACL_GUEST;
|
||||
} else {
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", data);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
client = acl.putClient(sender, 0); // add to contacts (if not already known)
|
||||
if (sender_timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
||||
return 0; // FATAL: client table is full -OR- replay attack
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->permissions |= perms;
|
||||
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
if (perms != PERM_ACL_GUEST) { // keep number of FS writes to a minimum
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16)
|
||||
reply_data[6] = client->isAdmin() ? 1 : 0;
|
||||
reply_data[7] = client->permissions;
|
||||
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
||||
reply_data[12] = FIRMWARE_VER_LEVEL; // New field
|
||||
|
||||
return 13; // reply length
|
||||
}
|
||||
|
||||
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
|
||||
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now
|
||||
RepeaterStats stats;
|
||||
stats.batt_milli_volts = board.getBattMilliVolts();
|
||||
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
|
||||
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
|
||||
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
|
||||
stats.n_packets_recv = radio_driver.getPacketsRecv();
|
||||
stats.n_packets_sent = radio_driver.getPacketsSent();
|
||||
stats.total_air_time_secs = getTotalAirTime() / 1000;
|
||||
stats.total_up_time_secs = _ms->getMillis() / 1000;
|
||||
stats.n_sent_flood = getNumSentFlood();
|
||||
stats.n_sent_direct = getNumSentDirect();
|
||||
stats.n_recv_flood = getNumRecvFlood();
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
|
||||
|
||||
memcpy(&reply_data[4], &stats, sizeof(stats));
|
||||
|
||||
return 4 + sizeof(stats); // reply_len
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
|
||||
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry);
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
|
||||
uint8_t res1 = payload[1]; // reserved for future (extra query params)
|
||||
uint8_t res2 = payload[2];
|
||||
if (res1 == 0 && res2 == 0) {
|
||||
uint8_t ofs = 4;
|
||||
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted entries
|
||||
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
|
||||
reply_data[ofs++] = c->permissions;
|
||||
}
|
||||
return ofs;
|
||||
}
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_NEIGHBOURS) {
|
||||
uint8_t request_version = payload[1];
|
||||
if (request_version == 0) {
|
||||
|
||||
// reply data offset (after response sender_timestamp/tag)
|
||||
int reply_offset = 4;
|
||||
|
||||
// get request params
|
||||
uint8_t count = payload[2]; // how many neighbours to fetch (0-255)
|
||||
uint16_t offset;
|
||||
memcpy(&offset, &payload[3], 2); // offset from start of neighbours list (0-65535)
|
||||
uint8_t order_by = payload[5]; // how to order neighbours. 0=newest_to_oldest, 1=oldest_to_newest, 2=strongest_to_weakest, 3=weakest_to_strongest
|
||||
uint8_t pubkey_prefix_length = payload[6]; // how many bytes of neighbour pub key we want
|
||||
// we also send a 4 byte random blob in payload[7...10] to help packet uniqueness
|
||||
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS count=%d, offset=%d, order_by=%d, pubkey_prefix_length=%d", count, offset, order_by, pubkey_prefix_length);
|
||||
|
||||
// clamp pub key prefix length to max pub key length
|
||||
if(pubkey_prefix_length > PUB_KEY_SIZE){
|
||||
pubkey_prefix_length = PUB_KEY_SIZE;
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS invalid pubkey_prefix_length=%d clamping to %d", pubkey_prefix_length, PUB_KEY_SIZE);
|
||||
}
|
||||
|
||||
// create copy of neighbours list, skipping empty entries so we can sort it separately from main list
|
||||
int16_t neighbours_count = 0;
|
||||
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
auto neighbour = &neighbours[i];
|
||||
if (neighbour->heard_timestamp > 0) {
|
||||
sorted_neighbours[neighbours_count] = neighbour;
|
||||
neighbours_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// sort neighbours based on order
|
||||
if (order_by == 0) {
|
||||
// sort by newest to oldest
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting newest to oldest");
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->heard_timestamp > b->heard_timestamp; // desc
|
||||
});
|
||||
} else if (order_by == 1) {
|
||||
// sort by oldest to newest
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting oldest to newest");
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->heard_timestamp < b->heard_timestamp; // asc
|
||||
});
|
||||
} else if (order_by == 2) {
|
||||
// sort by strongest to weakest
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting strongest to weakest");
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->snr > b->snr; // desc
|
||||
});
|
||||
} else if (order_by == 3) {
|
||||
// sort by weakest to strongest
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting weakest to strongest");
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->snr < b->snr; // asc
|
||||
});
|
||||
}
|
||||
|
||||
// build results buffer
|
||||
int results_count = 0;
|
||||
int results_offset = 0;
|
||||
uint8_t results_buffer[130];
|
||||
for(int index = 0; index < count && index + offset < neighbours_count; index++){
|
||||
|
||||
// stop if we can't fit another entry in results
|
||||
int entry_size = pubkey_prefix_length + 4 + 1;
|
||||
if(results_offset + entry_size > sizeof(results_buffer)){
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS no more entries can fit in results buffer");
|
||||
break;
|
||||
}
|
||||
|
||||
// add next neighbour to results
|
||||
auto neighbour = sorted_neighbours[index + offset];
|
||||
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
|
||||
memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length); results_offset += pubkey_prefix_length;
|
||||
memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4;
|
||||
memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1;
|
||||
results_count++;
|
||||
|
||||
}
|
||||
|
||||
// build reply
|
||||
MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS neighbours_count=%d results_count=%d", neighbours_count, results_count);
|
||||
memcpy(&reply_data[reply_offset], &neighbours_count, 2); reply_offset += 2;
|
||||
memcpy(&reply_data[reply_offset], &results_count, 2); reply_offset += 2;
|
||||
memcpy(&reply_data[reply_offset], &results_buffer, results_offset); reply_offset += results_offset;
|
||||
|
||||
return reply_offset;
|
||||
}
|
||||
}
|
||||
return 0; // unknown command
|
||||
}
|
||||
|
||||
mesh::Packet *MyMesh::createSelfAdvert() {
|
||||
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
||||
uint8_t app_data_len;
|
||||
{
|
||||
AdvertDataBuilder builder(ADV_TYPE_REPEATER, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
|
||||
app_data_len = builder.encodeTo(app_data);
|
||||
}
|
||||
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
File MyMesh::openAppend(const char *fname) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return _fs->open(fname, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(fname, "a");
|
||||
#else
|
||||
return _fs->open(fname, "a", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *MyMesh::getLogDateTime() {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
|
||||
dt.year());
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len,
|
||||
pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000));
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
|
||||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
#ifdef WITH_BRIDGE
|
||||
bridge.onPacketTransmitted(pkt);
|
||||
#endif
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(),
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
|
||||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(),
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
}
|
||||
|
||||
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6) * t;
|
||||
}
|
||||
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6) * t;
|
||||
}
|
||||
|
||||
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
|
||||
uint8_t *data, size_t len) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin
|
||||
// client (unknown at this stage)
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
|
||||
|
||||
if (reply_len == 0) return; // invalid request
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
||||
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int MyMesh::searchPeersByHash(const uint8_t *hash) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
|
||||
int i = matching_peer_indexes[peer_idx];
|
||||
if (i >= 0 && i < acl.getNumClients()) {
|
||||
// lookup pre-calculated shared_secret
|
||||
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp,
|
||||
const uint8_t *app_data, size_t app_data_len) {
|
||||
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl
|
||||
|
||||
// if this a zero hop advert, add it to neighbours
|
||||
if (packet->path_len == 0) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
|
||||
putNeighbour(id, timestamp, packet->getSNR());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret,
|
||||
uint8_t *data, size_t len) {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
|
||||
return;
|
||||
}
|
||||
ClientInfo* client = acl.getClientByIdx(i);
|
||||
|
||||
if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!)
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
if (timestamp > client->last_timestamp) { // prevent replay attacks
|
||||
int reply_len = handleRequest(client, timestamp, &data[4], len - 4);
|
||||
if (reply_len == 0) return; // invalid command
|
||||
|
||||
client->last_timestamp = timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet *reply =
|
||||
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove
|
||||
// to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key,
|
||||
PUB_KEY_SIZE);
|
||||
|
||||
mesh::Packet *ack = createAck(ack_hash);
|
||||
if (ack) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t temp[166];
|
||||
char *command = (char *)&data[5];
|
||||
char *reply = (char *)&temp[5];
|
||||
if (is_retry) {
|
||||
*reply = 0;
|
||||
} else {
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
}
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
if (timestamp == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
timestamp++;
|
||||
}
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path,
|
||||
uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) {
|
||||
// TODO: prevent replay attacks
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
|
||||
if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
|
||||
auto client = acl.getClientByIdx(i);
|
||||
|
||||
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
|
||||
}
|
||||
|
||||
// NOTE: no reciprocal path send!!
|
||||
return false;
|
||||
}
|
||||
|
||||
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
|
||||
mesh::RTCClock &rtc, mesh::MeshTables &tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
#if defined(WITH_RS232_BRIDGE)
|
||||
, bridge(WITH_RS232_BRIDGE, _mgr, &rtc)
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
, bridge(_mgr, &rtc)
|
||||
#endif
|
||||
{
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
_logging = false;
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
#endif
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0; // one half
|
||||
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f
|
||||
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
||||
_prefs.node_lat = ADVERT_LAT;
|
||||
_prefs.node_lon = ADVERT_LON;
|
||||
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
|
||||
_prefs.freq = LORA_FREQ;
|
||||
_prefs.sf = LORA_SF;
|
||||
_prefs.bw = LORA_BW;
|
||||
_prefs.cr = LORA_CR;
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
|
||||
_prefs.flood_advert_interval = 12; // 12 hours
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
}
|
||||
|
||||
void MyMesh::begin(FILESYSTEM *fs) {
|
||||
mesh::Mesh::begin();
|
||||
_fs = fs;
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
|
||||
acl.load(_fs);
|
||||
|
||||
#ifdef WITH_BRIDGE
|
||||
bridge.begin();
|
||||
#endif
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
}
|
||||
|
||||
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
||||
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
|
||||
pending_freq = freq;
|
||||
pending_bw = bw;
|
||||
pending_sf = sf;
|
||||
pending_cr = cr;
|
||||
|
||||
revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params
|
||||
}
|
||||
|
||||
bool MyMesh::formatFileSystem() {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return InternalFS.format();
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return LittleFS.format();
|
||||
#elif defined(ESP32)
|
||||
return SPIFFS.format();
|
||||
#else
|
||||
#error "need to implement file system erase"
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::sendSelfAdvertisement(int delay_millis) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::updateAdvertTimer() {
|
||||
if (_prefs.advert_interval > 0) { // schedule local advert timer
|
||||
next_local_advert = futureMillis(((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
|
||||
} else {
|
||||
next_local_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::updateFloodAdvertTimer() {
|
||||
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
|
||||
next_flood_advert = futureMillis(((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
|
||||
} else {
|
||||
next_flood_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::dumpLogFile() {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File f = _fs->open(PACKET_LOG_FILE, "r");
|
||||
#else
|
||||
File f = _fs->open(PACKET_LOG_FILE);
|
||||
#endif
|
||||
if (f) {
|
||||
while (f.available()) {
|
||||
int c = f.read();
|
||||
if (c < 0) break;
|
||||
Serial.print((char)c);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::setTxPower(uint8_t power_dbm) {
|
||||
radio_set_tx_power(power_dbm);
|
||||
}
|
||||
|
||||
void MyMesh::formatNeighborsReply(char *reply) {
|
||||
char *dp = reply;
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
// create copy of neighbours list, skipping empty entries so we can sort it separately from main list
|
||||
int16_t neighbours_count = 0;
|
||||
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
auto neighbour = &neighbours[i];
|
||||
if (neighbour->heard_timestamp > 0) {
|
||||
sorted_neighbours[neighbours_count] = neighbour;
|
||||
neighbours_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// sort neighbours newest to oldest
|
||||
std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) {
|
||||
return a->heard_timestamp > b->heard_timestamp; // desc
|
||||
});
|
||||
|
||||
for (int i = 0; i < neighbours_count && dp - reply < 134; i++) {
|
||||
NeighbourInfo *neighbour = sorted_neighbours[i];
|
||||
|
||||
// add new line if not first item
|
||||
if (i > 0) *dp++ = '\n';
|
||||
|
||||
char hex[10];
|
||||
// get 4 bytes of neighbour id as hex
|
||||
mesh::Utils::toHex(hex, neighbour->id.pub_key, 4);
|
||||
|
||||
// add next neighbour
|
||||
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
|
||||
sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr);
|
||||
while (*dp)
|
||||
dp++; // find end of string
|
||||
}
|
||||
#endif
|
||||
if (dp == reply) { // no neighbours, need empty response
|
||||
strcpy(dp, "-none-");
|
||||
dp += 6;
|
||||
}
|
||||
*dp = 0; // null terminator
|
||||
}
|
||||
|
||||
void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
|
||||
#if MAX_NEIGHBOURS
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
NeighbourInfo *neighbour = &neighbours[i];
|
||||
if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) {
|
||||
neighbours[i] = NeighbourInfo(); // clear neighbour entry
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||
self_id = new_id;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
IdentityStore store(*_fs, "");
|
||||
#elif defined(ESP32)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#else
|
||||
#error "need to define saveIdentity()"
|
||||
#endif
|
||||
store.save("_main", self_id);
|
||||
}
|
||||
|
||||
void MyMesh::clearStats() {
|
||||
radio_driver.resetStats();
|
||||
resetStats();
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
|
||||
while (*command == ' ')
|
||||
command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
memcpy(reply, command, 3); // reflect the prefix back
|
||||
reply += 3;
|
||||
command += 3;
|
||||
}
|
||||
|
||||
// handle ACL related commands
|
||||
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
||||
char* hex = &command[8];
|
||||
char* sp = strchr(hex, ' '); // look for separator char
|
||||
if (sp == NULL) {
|
||||
strcpy(reply, "Err - bad params");
|
||||
} else {
|
||||
*sp++ = 0; // replace space with null terminator
|
||||
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||
uint8_t perms = atoi(sp);
|
||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - invalid params");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Err - bad pubkey");
|
||||
}
|
||||
}
|
||||
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
|
||||
Serial.println("ACL:");
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted (or guest) entries
|
||||
|
||||
Serial.printf("%02X ", c->permissions);
|
||||
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf("\n");
|
||||
}
|
||||
reply[0] = 0;
|
||||
} else{
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::loop() {
|
||||
#ifdef WITH_BRIDGE
|
||||
bridge.loop();
|
||||
#endif
|
||||
|
||||
mesh::Mesh::loop();
|
||||
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) sendFlood(pkt);
|
||||
|
||||
updateFloodAdvertTimer(); // schedule next flood advert
|
||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) sendZeroHop(pkt);
|
||||
|
||||
updateAdvertTimer(); // schedule next local advert
|
||||
}
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
// is pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs);
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
}
|
||||
185
examples/simple_repeater/MyMesh.h
Normal file
185
examples/simple_repeater/MyMesh.h
Normal file
@@ -0,0 +1,185 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
#include "helpers/bridges/RS232Bridge.h"
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
#include "helpers/bridges/ESPNowBridge.h"
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#ifdef WITH_BRIDGE
|
||||
extern AbstractBridge* bridge;
|
||||
#endif
|
||||
|
||||
struct RepeaterStats {
|
||||
uint16_t batt_milli_volts;
|
||||
uint16_t curr_tx_queue_len;
|
||||
int16_t noise_floor;
|
||||
int16_t last_rssi;
|
||||
uint32_t n_packets_recv;
|
||||
uint32_t n_packets_sent;
|
||||
uint32_t total_air_time_secs;
|
||||
uint32_t total_up_time_secs;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint16_t err_events; // was 'n_full_events'
|
||||
int16_t last_snr; // x 4
|
||||
uint16_t n_direct_dups, n_flood_dups;
|
||||
uint32_t total_rx_air_time_secs;
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
struct NeighbourInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t advert_timestamp;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4, user should divide to get float value
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "28 Sep 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.9.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
FILESYSTEM* _fs;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
ClientACL acl;
|
||||
unsigned long dirty_contacts_expiry;
|
||||
#if MAX_NEIGHBOURS
|
||||
NeighbourInfo neighbours[MAX_NEIGHBOURS];
|
||||
#endif
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
#if defined(WITH_RS232_BRIDGE)
|
||||
RS232Bridge bridge;
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge bridge;
|
||||
#endif
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
|
||||
File openAppend(const char* fname);
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
return _prefs.airtime_factor;
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
const char* getLogDateTime() override;
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override;
|
||||
void logTx(mesh::Packet* pkt, int len) override;
|
||||
void logTxFail(mesh::Packet* pkt, int len) override;
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
||||
|
||||
int getInterferenceThreshold() const override {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
int getAGCResetInterval() const override {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
uint8_t getExtraAckTransmitCount() const override {
|
||||
return _prefs.multi_acks;
|
||||
}
|
||||
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||
int searchPeersByHash(const uint8_t* hash) override;
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
|
||||
void begin(FILESYSTEM* fs);
|
||||
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
}
|
||||
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
bool formatFileSystem() override;
|
||||
void sendSelfAdvertisement(int delay_millis) override;
|
||||
void updateAdvertTimer() override;
|
||||
void updateFloodAdvertTimer() override;
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
|
||||
void eraseLogFile() override {
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
}
|
||||
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(uint8_t power_dbm) override;
|
||||
void formatNeighborsReply(char *reply) override;
|
||||
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
|
||||
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void loop();
|
||||
};
|
||||
@@ -1,742 +1,13 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "15 Jul 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.7.3"
|
||||
#endif
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
#define LORA_FREQ 915.0
|
||||
#endif
|
||||
#ifndef LORA_BW
|
||||
#define LORA_BW 250
|
||||
#endif
|
||||
#ifndef LORA_SF
|
||||
#define LORA_SF 10
|
||||
#endif
|
||||
#ifndef LORA_CR
|
||||
#define LORA_CR 5
|
||||
#endif
|
||||
#ifndef LORA_TX_POWER
|
||||
#define LORA_TX_POWER 20
|
||||
#endif
|
||||
|
||||
#ifndef ADVERT_NAME
|
||||
#define ADVERT_NAME "repeater"
|
||||
#endif
|
||||
#ifndef ADVERT_LAT
|
||||
#define ADVERT_LAT 0.0
|
||||
#endif
|
||||
#ifndef ADVERT_LON
|
||||
#define ADVERT_LON 0.0
|
||||
#endif
|
||||
|
||||
#ifndef ADMIN_PASSWORD
|
||||
#define ADMIN_PASSWORD "password"
|
||||
#endif
|
||||
|
||||
#ifndef SERVER_RESPONSE_DELAY
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
#endif
|
||||
|
||||
#ifndef TXT_ACK_DELAY
|
||||
#define TXT_ACK_DELAY 200
|
||||
#endif
|
||||
#include "MyMesh.h"
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
static UITask ui_task(display);
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "repeater"
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
/* ------------------------------ Code -------------------------------- */
|
||||
|
||||
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
struct RepeaterStats {
|
||||
uint16_t batt_milli_volts;
|
||||
uint16_t curr_tx_queue_len;
|
||||
int16_t noise_floor;
|
||||
int16_t last_rssi;
|
||||
uint32_t n_packets_recv;
|
||||
uint32_t n_packets_sent;
|
||||
uint32_t total_air_time_secs;
|
||||
uint32_t total_up_time_secs;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint16_t err_events; // was 'n_full_events'
|
||||
int16_t last_snr; // x 4
|
||||
uint16_t n_direct_dups, n_flood_dups;
|
||||
};
|
||||
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t last_timestamp, last_activity;
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
bool is_admin;
|
||||
int8_t out_path_len;
|
||||
uint8_t out_path[MAX_PATH_SIZE];
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
struct NeighbourInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t advert_timestamp;
|
||||
uint32_t heard_timestamp;
|
||||
int8_t snr; // multiplied by 4, user should divide to get float value
|
||||
};
|
||||
|
||||
#define CLI_REPLY_DELAY_MILLIS 1000
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
FILESYSTEM* _fs;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
ClientInfo known_clients[MAX_CLIENTS];
|
||||
#if MAX_NEIGHBOURS
|
||||
NeighbourInfo neighbours[MAX_NEIGHBOURS];
|
||||
#endif
|
||||
CayenneLPP telemetry;
|
||||
|
||||
ClientInfo* putClient(const mesh::Identity& id) {
|
||||
uint32_t min_time = 0xFFFFFFFF;
|
||||
ClientInfo* oldest = &known_clients[0];
|
||||
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||
if (known_clients[i].last_activity < min_time) {
|
||||
oldest = &known_clients[i];
|
||||
min_time = oldest->last_activity;
|
||||
}
|
||||
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
|
||||
}
|
||||
|
||||
oldest->id = id;
|
||||
oldest->out_path_len = -1; // initially out_path is unknown
|
||||
oldest->last_timestamp = 0;
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) {
|
||||
#if MAX_NEIGHBOURS // check if neighbours enabled
|
||||
// find existing neighbour, else use least recently updated
|
||||
uint32_t oldest_timestamp = 0xFFFFFFFF;
|
||||
NeighbourInfo* neighbour = &neighbours[0];
|
||||
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
|
||||
// if neighbour already known, we should update it
|
||||
if (id.matches(neighbours[i].id)) {
|
||||
neighbour = &neighbours[i];
|
||||
break;
|
||||
}
|
||||
|
||||
// otherwise we should update the least recently updated neighbour
|
||||
if (neighbours[i].heard_timestamp < oldest_timestamp) {
|
||||
neighbour = &neighbours[i];
|
||||
oldest_timestamp = neighbour->heard_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// update neighbour info
|
||||
neighbour->id = id;
|
||||
neighbour->advert_timestamp = timestamp;
|
||||
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
|
||||
neighbour->snr = (int8_t) (snr * 4);
|
||||
#endif
|
||||
}
|
||||
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) {
|
||||
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
switch (payload[0]) {
|
||||
case REQ_TYPE_GET_STATUS: { // guests can also access this now
|
||||
RepeaterStats stats;
|
||||
stats.batt_milli_volts = board.getBattMilliVolts();
|
||||
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
|
||||
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
|
||||
stats.last_rssi = (int16_t) radio_driver.getLastRSSI();
|
||||
stats.n_packets_recv = radio_driver.getPacketsRecv();
|
||||
stats.n_packets_sent = radio_driver.getPacketsSent();
|
||||
stats.total_air_time_secs = getTotalAirTime() / 1000;
|
||||
stats.total_up_time_secs = _ms->getMillis() / 1000;
|
||||
stats.n_sent_flood = getNumSentFlood();
|
||||
stats.n_sent_direct = getNumSentDirect();
|
||||
stats.n_recv_flood = getNumRecvFlood();
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
|
||||
memcpy(&reply_data[4], &stats, sizeof(stats));
|
||||
|
||||
return 4 + sizeof(stats); // reply_len
|
||||
}
|
||||
case REQ_TYPE_GET_TELEMETRY_DATA: {
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors(sender->is_admin ? 0xFF : 0x00, telemetry);
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
}
|
||||
return 0; // unknown command
|
||||
}
|
||||
|
||||
mesh::Packet* createSelfAdvert() {
|
||||
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
||||
uint8_t app_data_len;
|
||||
{
|
||||
AdvertDataBuilder builder(ADV_TYPE_REPEATER, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
|
||||
app_data_len = builder.encodeTo(app_data);
|
||||
}
|
||||
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
File openAppend(const char* fname) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return _fs->open(fname, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(fname, "a");
|
||||
#else
|
||||
return _fs->open(fname, "a", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
return _prefs.airtime_factor;
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* getLogDateTime() override {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year());
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
#endif
|
||||
}
|
||||
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|
||||
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void logTx(mesh::Packet* pkt, int len) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|
||||
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void logTxFail(mesh::Packet* pkt, int len) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int calcRxDelay(float score, uint32_t air_time) const override {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
}
|
||||
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6)*t;
|
||||
}
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6)*t;
|
||||
}
|
||||
int getInterferenceThreshold() const override {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
int getAGCResetInterval() const override {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
bool is_admin;
|
||||
data[len] = 0; // ensure null terminator
|
||||
if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password
|
||||
is_admin = true;
|
||||
} else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password
|
||||
is_admin = false;
|
||||
} else {
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = putClient(sender); // add to known clients (if not already known)
|
||||
if (timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
||||
return; // FATAL: client table is full -OR- replay attack
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->is_admin = is_admin;
|
||||
memcpy(client->secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
#if 0
|
||||
memcpy(&reply_data[4], "OK", 2); // legacy response
|
||||
#else
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
|
||||
reply_data[6] = is_admin ? 1 : 0;
|
||||
reply_data[7] = 0; // FUTURE: reserved
|
||||
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
||||
#endif
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, 12);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
|
||||
int searchPeersByHash(const uint8_t* hash) override {
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||
if (known_clients[i].id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
|
||||
int i = matching_peer_indexes[peer_idx];
|
||||
if (i >= 0 && i < MAX_CLIENTS) {
|
||||
// lookup pre-calculated shared_secret
|
||||
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
|
||||
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl
|
||||
|
||||
// if this a zero hop advert, add it to neighbours
|
||||
if (packet->path_len == 0) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
|
||||
putNeighbour(id, timestamp, packet->getSNR());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
|
||||
return;
|
||||
}
|
||||
auto client = &known_clients[i];
|
||||
if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!)
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
if (timestamp > client->last_timestamp) { // prevent replay attacks
|
||||
int reply_len = handleRequest(client, timestamp, &data[4], len - 4);
|
||||
if (reply_len == 0) return; // invalid command
|
||||
|
||||
client->last_timestamp = timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->is_admin) { // a CLI command
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t temp[166];
|
||||
char *command = (char *) &data[5];
|
||||
char *reply = (char *) &temp[5];
|
||||
if (is_retry) {
|
||||
*reply = 0;
|
||||
} else {
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
}
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
if (timestamp == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
timestamp++;
|
||||
}
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
|
||||
// TODO: prevent replay attacks
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
|
||||
if (i >= 0 && i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len);
|
||||
auto client = &known_clients[i];
|
||||
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
|
||||
}
|
||||
|
||||
// NOTE: no reciprocal path send!!
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
{
|
||||
memset(known_clients, 0, sizeof(known_clients));
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
_logging = false;
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
#endif
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0; // one half
|
||||
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f
|
||||
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
||||
_prefs.node_lat = ADVERT_LAT;
|
||||
_prefs.node_lon = ADVERT_LON;
|
||||
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
|
||||
_prefs.freq = LORA_FREQ;
|
||||
_prefs.sf = LORA_SF;
|
||||
_prefs.bw = LORA_BW;
|
||||
_prefs.cr = LORA_CR;
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
|
||||
_prefs.flood_advert_interval = 3; // 3 hours
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
}
|
||||
|
||||
void begin(FILESYSTEM* fs) {
|
||||
mesh::Mesh::begin();
|
||||
_fs = fs;
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
}
|
||||
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
}
|
||||
|
||||
bool formatFileSystem() override {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
return InternalFS.format();
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return LittleFS.format();
|
||||
#elif defined(ESP32)
|
||||
return SPIFFS.format();
|
||||
#else
|
||||
#error "need to implement file system erase"
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void sendSelfAdvertisement(int delay_millis) override {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
|
||||
}
|
||||
}
|
||||
|
||||
void updateAdvertTimer() override {
|
||||
if (_prefs.advert_interval > 0) { // schedule local advert timer
|
||||
next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
|
||||
} else {
|
||||
next_local_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
void updateFloodAdvertTimer() override {
|
||||
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
|
||||
next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
|
||||
} else {
|
||||
next_flood_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
|
||||
void eraseLogFile() override {
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
}
|
||||
|
||||
void dumpLogFile() override {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File f = _fs->open(PACKET_LOG_FILE, "r");
|
||||
#else
|
||||
File f = _fs->open(PACKET_LOG_FILE);
|
||||
#endif
|
||||
if (f) {
|
||||
while (f.available()) {
|
||||
int c = f.read();
|
||||
if (c < 0) break;
|
||||
Serial.print((char)c);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void setTxPower(uint8_t power_dbm) override {
|
||||
radio_set_tx_power(power_dbm);
|
||||
}
|
||||
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
char *dp = reply;
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
for (int i = 0; i < MAX_NEIGHBOURS && dp - reply < 134; i++) {
|
||||
NeighbourInfo* neighbour = &neighbours[i];
|
||||
if (neighbour->heard_timestamp == 0) continue; // skip empty slots
|
||||
|
||||
// add new line if not first item
|
||||
if (i > 0) *dp++ = '\n';
|
||||
|
||||
char hex[10];
|
||||
// get 4 bytes of neighbour id as hex
|
||||
mesh::Utils::toHex(hex, neighbour->id.pub_key, 4);
|
||||
|
||||
// add next neighbour
|
||||
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
|
||||
sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr);
|
||||
while (*dp) dp++; // find end of string
|
||||
}
|
||||
#endif
|
||||
if (dp == reply) { // no neighbours, need empty response
|
||||
strcpy(dp, "-none-"); dp += 6;
|
||||
}
|
||||
*dp = 0; // null terminator
|
||||
}
|
||||
|
||||
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
|
||||
|
||||
void clearStats() override {
|
||||
radio_driver.resetStats();
|
||||
resetStats();
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
while (*command == ' ') command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
memcpy(reply, command, 3); // reflect the prefix back
|
||||
reply += 3;
|
||||
command += 3;
|
||||
}
|
||||
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mesh::Mesh::loop();
|
||||
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) sendFlood(pkt);
|
||||
|
||||
updateFloodAdvertTimer(); // schedule next flood advert
|
||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) sendZeroHop(pkt);
|
||||
|
||||
updateAdvertTimer(); // schedule next local advert
|
||||
}
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.loop();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
|
||||
@@ -746,7 +17,7 @@ void halt() {
|
||||
while (1) ;
|
||||
}
|
||||
|
||||
static char command[80];
|
||||
static char command[160];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
@@ -757,12 +28,15 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display.begin()) {
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!radio_init()) { halt(); }
|
||||
if (!radio_init()) {
|
||||
halt();
|
||||
}
|
||||
|
||||
fast_rng.begin(radio_get_rng_seed());
|
||||
|
||||
@@ -837,4 +111,7 @@ void loop() {
|
||||
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.loop();
|
||||
#endif
|
||||
}
|
||||
|
||||
855
examples/simple_room_server/MyMesh.cpp
Normal file
855
examples/simple_room_server/MyMesh.cpp
Normal file
@@ -0,0 +1,855 @@
|
||||
#include "MyMesh.h"
|
||||
|
||||
#define REPLY_DELAY_MILLIS 1500
|
||||
#define PUSH_NOTIFY_DELAY_MILLIS 2000
|
||||
#define SYNC_PUSH_INTERVAL 1200
|
||||
|
||||
#define PUSH_ACK_TIMEOUT_FLOOD 12000
|
||||
#define PUSH_TIMEOUT_BASE 4000
|
||||
#define PUSH_ACK_TIMEOUT_FACTOR 2000
|
||||
|
||||
#define POST_SYNC_DELAY_SECS 6
|
||||
|
||||
#define FIRMWARE_VER_LEVEL 1
|
||||
|
||||
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||
|
||||
struct ServerStats {
|
||||
uint16_t batt_milli_volts;
|
||||
uint16_t curr_tx_queue_len;
|
||||
int16_t noise_floor;
|
||||
int16_t last_rssi;
|
||||
uint32_t n_packets_recv;
|
||||
uint32_t n_packets_sent;
|
||||
uint32_t total_air_time_secs;
|
||||
uint32_t total_up_time_secs;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint16_t err_events; // was 'n_full_events'
|
||||
int16_t last_snr; // x 4
|
||||
uint16_t n_direct_dups, n_flood_dups;
|
||||
uint16_t n_posted, n_post_push;
|
||||
};
|
||||
|
||||
void MyMesh::addPost(ClientInfo *client, const char *postData) {
|
||||
// TODO: suggested postData format: <title>/<descrption>
|
||||
posts[next_post_idx].author = client->id; // add to cyclic queue
|
||||
StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
|
||||
|
||||
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS;
|
||||
|
||||
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);
|
||||
_num_posted++; // stats
|
||||
}
|
||||
|
||||
void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
|
||||
int len = 0;
|
||||
memcpy(&reply_data[len], &post.post_timestamp, 4);
|
||||
len += 4; // this is a PAST timestamp... but should be accepted by client
|
||||
|
||||
uint8_t attempt;
|
||||
getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different
|
||||
reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text
|
||||
|
||||
// encode prefix of post.author.pub_key
|
||||
memcpy(&reply_data[len], post.author.pub_key, 4);
|
||||
len += 4; // just first 4 bytes
|
||||
|
||||
int text_len = strlen(post.text);
|
||||
memcpy(&reply_data[len], post.text, text_len);
|
||||
len += text_len;
|
||||
|
||||
// calc expected ACK reply
|
||||
mesh::Utils::sha256((uint8_t *)&client->extra.room.pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
|
||||
client->extra.room.push_post_timestamp = post.post_timestamp;
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply);
|
||||
client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len);
|
||||
client->extra.room.ack_timeout =
|
||||
futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
|
||||
}
|
||||
_num_post_pushes++; // stats
|
||||
} else {
|
||||
client->extra.room.pending_ack = 0;
|
||||
MESH_DEBUG_PRINTLN("Unable to push post to client");
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t MyMesh::getUnsyncedCount(ClientInfo *client) {
|
||||
uint8_t count = 0;
|
||||
for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) {
|
||||
if (posts[k].post_timestamp > client->extra.room.sync_since // is new post for this Client?
|
||||
&& !posts[k].author.matches(client->id)) { // don't push posts to the author
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool MyMesh::processAck(const uint8_t *data) {
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto client = acl.getClientByIdx(i);
|
||||
if (client->extra.room.pending_ack && memcmp(data, &client->extra.room.pending_ack, 4) == 0) { // got an ACK from Client!
|
||||
client->extra.room.pending_ack = 0; // clear this, so next push can happen
|
||||
client->extra.room.push_failures = 0;
|
||||
client->extra.room.sync_since = client->extra.room.push_post_timestamp; // advance Client's SINCE timestamp, to sync next post
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mesh::Packet *MyMesh::createSelfAdvert() {
|
||||
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
||||
uint8_t app_data_len;
|
||||
{
|
||||
AdvertDataBuilder builder(ADV_TYPE_ROOM, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
|
||||
app_data_len = builder.encodeTo(app_data);
|
||||
}
|
||||
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
File MyMesh::openAppend(const char *fname) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return _fs->open(fname, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(fname, "a");
|
||||
#else
|
||||
return _fs->open(fname, "a", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload,
|
||||
size_t payload_len) {
|
||||
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
if (payload[0] == REQ_TYPE_GET_STATUS) {
|
||||
ServerStats stats;
|
||||
stats.batt_milli_volts = board.getBattMilliVolts();
|
||||
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
|
||||
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
|
||||
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
|
||||
stats.n_packets_recv = radio_driver.getPacketsRecv();
|
||||
stats.n_packets_sent = radio_driver.getPacketsSent();
|
||||
stats.total_air_time_secs = getTotalAirTime() / 1000;
|
||||
stats.total_up_time_secs = _ms->getMillis() / 1000;
|
||||
stats.n_sent_flood = getNumSentFlood();
|
||||
stats.n_sent_direct = getNumSentDirect();
|
||||
stats.n_recv_flood = getNumRecvFlood();
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
stats.n_posted = _num_posted;
|
||||
stats.n_post_push = _num_post_pushes;
|
||||
|
||||
memcpy(&reply_data[4], &stats, sizeof(stats));
|
||||
return 4 + sizeof(stats);
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
|
||||
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry);
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
|
||||
uint8_t res1 = payload[1]; // reserved for future (extra query params)
|
||||
uint8_t res2 = payload[2];
|
||||
if (res1 == 0 && res2 == 0) {
|
||||
uint8_t ofs = 4;
|
||||
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (!c->isAdmin()) continue; // skip non-Admin entries
|
||||
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
|
||||
reply_data[ofs++] = c->permissions;
|
||||
}
|
||||
return ofs;
|
||||
}
|
||||
}
|
||||
return 0; // unknown command
|
||||
}
|
||||
|
||||
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len,
|
||||
pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000));
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
|
||||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(),
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ ||
|
||||
pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(),
|
||||
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
}
|
||||
|
||||
const char *MyMesh::getLogDateTime() {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
|
||||
dt.year());
|
||||
return tmp;
|
||||
}
|
||||
|
||||
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6) * t;
|
||||
}
|
||||
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6) * t;
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
|
||||
uint8_t *data, size_t len) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin
|
||||
// client (unknown at this stage)
|
||||
uint32_t sender_timestamp, sender_sync_since;
|
||||
memcpy(&sender_timestamp, data, 4);
|
||||
memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp
|
||||
|
||||
data[len] = 0; // ensure null terminator
|
||||
|
||||
ClientInfo* client = NULL;
|
||||
if (data[8] == 0 && !_prefs.allow_read_only) { // blank password, just check if sender is in ACL
|
||||
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
|
||||
if (client == NULL) {
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (client == NULL) {
|
||||
uint8_t perm;
|
||||
if (strcmp((char *)&data[8], _prefs.password) == 0) { // check for valid admin password
|
||||
perm = PERM_ACL_ADMIN;
|
||||
} else {
|
||||
if (strcmp((char *)&data[8], _prefs.guest_password) == 0) { // check the room/public password
|
||||
perm = PERM_ACL_READ_WRITE;
|
||||
} else if (_prefs.allow_read_only) {
|
||||
perm = PERM_ACL_GUEST;
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("Incorrect room password");
|
||||
return; // no response. Client will timeout
|
||||
}
|
||||
}
|
||||
|
||||
client = acl.putClient(sender, 0); // add to known clients (if not already known)
|
||||
if (sender_timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("possible replay attack!");
|
||||
return;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->extra.room.sync_since = sender_sync_since;
|
||||
client->extra.room.pending_ack = 0;
|
||||
client->extra.room.push_failures = 0;
|
||||
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->permissions |= perm;
|
||||
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
// TODO: maybe reply with count of messages waiting to be synced for THIS client?
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16)
|
||||
reply_data[6] = (client->isAdmin() ? 1 : (client->permissions == 0 ? 2 : 0));
|
||||
// LEGACY: reply_data[7] = getUnsyncedCount(client);
|
||||
reply_data[7] = client->permissions; // NEW
|
||||
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
||||
reply_data[12] = FIRMWARE_VER_LEVEL; // New field
|
||||
|
||||
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, 13);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int MyMesh::searchPeersByHash(const uint8_t *hash) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
|
||||
int i = matching_peer_indexes[peer_idx];
|
||||
if (i >= 0 && i < acl.getNumClients()) {
|
||||
// lookup pre-calculated shared_secret
|
||||
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret,
|
||||
uint8_t *data, size_t len) {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
|
||||
return;
|
||||
}
|
||||
auto client = acl.getClientByIdx(i);
|
||||
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
client->last_timestamp = sender_timestamp;
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
client->last_activity = now;
|
||||
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to
|
||||
// sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key,
|
||||
PUB_KEY_SIZE);
|
||||
|
||||
uint8_t temp[166];
|
||||
bool send_ack;
|
||||
if (flags == TXT_TYPE_CLI_DATA) {
|
||||
if (client->isAdmin()) {
|
||||
if (is_retry) {
|
||||
temp[5] = 0; // no reply
|
||||
} else {
|
||||
handleCommand(sender_timestamp, (char *)&data[5], (char *)&temp[5]);
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
|
||||
}
|
||||
send_ack = false;
|
||||
} else {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // and no ACK... user shoudn't be sending these
|
||||
}
|
||||
} else { // TXT_TYPE_PLAIN
|
||||
if ((client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // no ACK
|
||||
} else {
|
||||
if (!is_retry) {
|
||||
addPost(client, (const char *)&data[5]);
|
||||
}
|
||||
temp[5] = 0; // no reply (ACK is enough)
|
||||
send_ack = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t delay_millis;
|
||||
if (send_ack) {
|
||||
if (client->out_path_len < 0) {
|
||||
mesh::Packet *ack = createAck(ack_hash);
|
||||
if (ack) sendFlood(ack, TXT_ACK_DELAY);
|
||||
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet *a1 = createMultiAck(ack_hash, 1);
|
||||
if (a1) sendDirect(a1, client->out_path, client->out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet *a2 = createAck(ack_hash);
|
||||
if (a2) sendDirect(a2, client->out_path, client->out_path_len, d);
|
||||
delay_millis = d + REPLY_DELAY_MILLIS;
|
||||
}
|
||||
} else {
|
||||
delay_millis = 0;
|
||||
}
|
||||
|
||||
int text_len = strlen((char *)&temp[5]);
|
||||
if (text_len > 0) {
|
||||
if (now == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
now++;
|
||||
}
|
||||
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
|
||||
|
||||
// calc expected ACK reply
|
||||
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
|
||||
// PUB_KEY_SIZE);
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_REQ && len >= 5) {
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
if (sender_timestamp < client->last_timestamp) { // prevent replay attacks
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
} else {
|
||||
client->last_timestamp = sender_timestamp;
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
client->last_activity = now; // <-- THIS will keep client connection alive
|
||||
client->extra.room.push_failures = 0; // reset so push can resume (if prev failed)
|
||||
|
||||
if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type
|
||||
uint32_t forceSince = 0;
|
||||
if (len >= 9) { // optional - last post_timestamp client received
|
||||
memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING!
|
||||
} else {
|
||||
memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below)
|
||||
}
|
||||
if (forceSince > 0) {
|
||||
client->extra.room.sync_since = forceSince; // force-update the 'sync since'
|
||||
}
|
||||
|
||||
client->extra.room.pending_ack = 0;
|
||||
|
||||
// TODO: Throttle KEEP_ALIVE requests!
|
||||
// if client sends too quickly, evict()
|
||||
|
||||
// RULE: only send keep_alive response DIRECT!
|
||||
if (client->out_path_len >= 0) {
|
||||
uint32_t ack_hash; // calc ACK to prove to sender that we got request
|
||||
mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
auto reply = createAck(ack_hash);
|
||||
if (reply) {
|
||||
reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4);
|
||||
if (reply_len > 0) { // valid command
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path,
|
||||
uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) {
|
||||
// TODO: prevent replay attacks
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
|
||||
if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
|
||||
auto client = acl.getClientByIdx(i);
|
||||
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
|
||||
}
|
||||
|
||||
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
||||
// also got an encoded ACK!
|
||||
processAck(extra);
|
||||
}
|
||||
|
||||
// NOTE: no reciprocal path send!!
|
||||
return false;
|
||||
}
|
||||
|
||||
void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
|
||||
if (processAck((uint8_t *)&ack_crc)) {
|
||||
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
||||
}
|
||||
}
|
||||
|
||||
MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
|
||||
mesh::RTCClock &rtc, mesh::MeshTables &tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
_logging = false;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0; // one half
|
||||
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
|
||||
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
||||
_prefs.node_lat = ADVERT_LAT;
|
||||
_prefs.node_lon = ADVERT_LON;
|
||||
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
|
||||
_prefs.freq = LORA_FREQ;
|
||||
_prefs.sf = LORA_SF;
|
||||
_prefs.bw = LORA_BW;
|
||||
_prefs.cr = LORA_CR;
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.disable_fwd = 1;
|
||||
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
|
||||
_prefs.flood_advert_interval = 12; // 12 hours
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
#ifdef ROOM_PASSWORD
|
||||
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
|
||||
#endif
|
||||
|
||||
next_post_idx = 0;
|
||||
next_client_idx = 0;
|
||||
next_push = 0;
|
||||
memset(posts, 0, sizeof(posts));
|
||||
_num_posted = _num_post_pushes = 0;
|
||||
}
|
||||
|
||||
void MyMesh::begin(FILESYSTEM *fs) {
|
||||
mesh::Mesh::begin();
|
||||
_fs = fs;
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
|
||||
acl.load(_fs);
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
}
|
||||
|
||||
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
||||
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
|
||||
pending_freq = freq;
|
||||
pending_bw = bw;
|
||||
pending_sf = sf;
|
||||
pending_cr = cr;
|
||||
|
||||
revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params
|
||||
}
|
||||
|
||||
bool MyMesh::formatFileSystem() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return InternalFS.format();
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return LittleFS.format();
|
||||
#elif defined(ESP32)
|
||||
return SPIFFS.format();
|
||||
#else
|
||||
#error "need to implement file system erase"
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MyMesh::sendSelfAdvertisement(int delay_millis) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::updateAdvertTimer() {
|
||||
if (_prefs.advert_interval > 0) { // schedule local advert timer
|
||||
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
|
||||
} else {
|
||||
next_local_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
void MyMesh::updateFloodAdvertTimer() {
|
||||
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
|
||||
next_flood_advert = futureMillis(((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
|
||||
} else {
|
||||
next_flood_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::dumpLogFile() {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File f = _fs->open(PACKET_LOG_FILE, "r");
|
||||
#else
|
||||
File f = _fs->open(PACKET_LOG_FILE);
|
||||
#endif
|
||||
if (f) {
|
||||
while (f.available()) {
|
||||
int c = f.read();
|
||||
if (c < 0) break;
|
||||
Serial.print((char)c);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::setTxPower(uint8_t power_dbm) {
|
||||
radio_set_tx_power(power_dbm);
|
||||
}
|
||||
|
||||
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
|
||||
self_id = new_id;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
IdentityStore store(*_fs, "");
|
||||
#elif defined(ESP32)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#else
|
||||
#error "need to define saveIdentity()"
|
||||
#endif
|
||||
store.save("_main", self_id);
|
||||
}
|
||||
|
||||
void MyMesh::clearStats() {
|
||||
radio_driver.resetStats();
|
||||
resetStats();
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
|
||||
while (*command == ' ')
|
||||
command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
memcpy(reply, command, 3); // reflect the prefix back
|
||||
reply += 3;
|
||||
command += 3;
|
||||
}
|
||||
|
||||
// handle ACL related commands
|
||||
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
||||
char* hex = &command[8];
|
||||
char* sp = strchr(hex, ' '); // look for separator char
|
||||
if (sp == NULL) {
|
||||
strcpy(reply, "Err - bad params");
|
||||
} else {
|
||||
*sp++ = 0; // replace space with null terminator
|
||||
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||
uint8_t perms = atoi(sp);
|
||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - invalid params");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Err - bad pubkey");
|
||||
}
|
||||
}
|
||||
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
|
||||
Serial.println("ACL:");
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted (or guest) entries
|
||||
|
||||
Serial.printf("%02X ", c->permissions);
|
||||
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf("\n");
|
||||
}
|
||||
reply[0] = 0;
|
||||
} else{
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::saveFilter(ClientInfo* client) {
|
||||
return client->isAdmin(); // only save Admins
|
||||
}
|
||||
|
||||
void MyMesh::loop() {
|
||||
mesh::Mesh::loop();
|
||||
|
||||
if (millisHasNowPassed(next_push) && acl.getNumClients() > 0) {
|
||||
// check for ACK timeouts
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->extra.room.pending_ack && millisHasNowPassed(c->extra.room.ack_timeout)) {
|
||||
c->extra.room.push_failures++;
|
||||
c->extra.room.pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
|
||||
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
|
||||
}
|
||||
}
|
||||
// check next Round-Robin client, and sync next new post
|
||||
auto client = acl.getClientByIdx(next_client_idx);
|
||||
bool did_push = false;
|
||||
if (client->extra.room.pending_ack == 0 && client->last_activity != 0 &&
|
||||
client->extra.room.push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max
|
||||
MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t)client->id.pub_key[0]);
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) {
|
||||
auto p = &posts[idx];
|
||||
if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS &&
|
||||
p->post_timestamp > client->extra.room.sync_since // is new post for this Client?
|
||||
&& !p->author.matches(client->id)) { // don't push posts to the author
|
||||
// push this post to Client, then wait for ACK
|
||||
pushPostToClient(client, *p);
|
||||
did_push = true;
|
||||
MESH_DEBUG_PRINTLN("loop - pushed to client %02X: %s", (uint32_t)client->id.pub_key[0], p->text);
|
||||
break;
|
||||
}
|
||||
idx = (idx + 1) % MAX_UNSYNCED_POSTS; // wrap to start of cyclic queue
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t)client->id.pub_key[0]);
|
||||
}
|
||||
next_client_idx = (next_client_idx + 1) % acl.getNumClients(); // round robin polling for each client
|
||||
|
||||
if (did_push) {
|
||||
next_push = futureMillis(SYNC_PUSH_INTERVAL);
|
||||
} else {
|
||||
// were no unsynced posts for curr client, so proccess next client much quicker! (in next loop())
|
||||
next_push = futureMillis(SYNC_PUSH_INTERVAL / 8);
|
||||
}
|
||||
}
|
||||
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) sendFlood(pkt);
|
||||
|
||||
updateFloodAdvertTimer(); // schedule next flood advert
|
||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
|
||||
mesh::Packet *pkt = createSelfAdvert();
|
||||
if (pkt) sendZeroHop(pkt);
|
||||
|
||||
updateAdvertTimer(); // schedule next local advert
|
||||
}
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
// is pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs, MyMesh::saveFilter);
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
|
||||
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
|
||||
}
|
||||
196
examples/simple_room_server/MyMesh.h
Normal file
196
examples/simple_room_server/MyMesh.h
Normal file
@@ -0,0 +1,196 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "28 Sep 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.9.0"
|
||||
#endif
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
#define LORA_FREQ 915.0
|
||||
#endif
|
||||
#ifndef LORA_BW
|
||||
#define LORA_BW 250
|
||||
#endif
|
||||
#ifndef LORA_SF
|
||||
#define LORA_SF 10
|
||||
#endif
|
||||
#ifndef LORA_CR
|
||||
#define LORA_CR 5
|
||||
#endif
|
||||
#ifndef LORA_TX_POWER
|
||||
#define LORA_TX_POWER 20
|
||||
#endif
|
||||
|
||||
#ifndef ADVERT_NAME
|
||||
#define ADVERT_NAME "Test BBS"
|
||||
#endif
|
||||
#ifndef ADVERT_LAT
|
||||
#define ADVERT_LAT 0.0
|
||||
#endif
|
||||
#ifndef ADVERT_LON
|
||||
#define ADVERT_LON 0.0
|
||||
#endif
|
||||
|
||||
#ifndef ADMIN_PASSWORD
|
||||
#define ADMIN_PASSWORD "password"
|
||||
#endif
|
||||
|
||||
#ifndef MAX_UNSYNCED_POSTS
|
||||
#define MAX_UNSYNCED_POSTS 32
|
||||
#endif
|
||||
|
||||
#ifndef SERVER_RESPONSE_DELAY
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
#endif
|
||||
|
||||
#ifndef TXT_ACK_DELAY
|
||||
#define TXT_ACK_DELAY 200
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "room_server"
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
#define MAX_POST_TEXT_LEN (160-9)
|
||||
|
||||
struct PostInfo {
|
||||
mesh::Identity author;
|
||||
uint32_t post_timestamp; // by OUR clock
|
||||
char text[MAX_POST_TEXT_LEN+1];
|
||||
};
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
FILESYSTEM* _fs;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
ClientACL acl;
|
||||
unsigned long dirty_contacts_expiry;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
unsigned long next_push;
|
||||
uint16_t _num_posted, _num_post_pushes;
|
||||
int next_client_idx; // for round-robin polling
|
||||
int next_post_idx;
|
||||
PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
|
||||
void addPost(ClientInfo* client, const char* postData);
|
||||
void pushPostToClient(ClientInfo* client, PostInfo& post);
|
||||
uint8_t getUnsyncedCount(ClientInfo* client);
|
||||
bool processAck(const uint8_t *data);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
File openAppend(const char* fname);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
return _prefs.airtime_factor;
|
||||
}
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override;
|
||||
void logTx(mesh::Packet* pkt, int len) override;
|
||||
void logTxFail(mesh::Packet* pkt, int len) override;
|
||||
|
||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||
const char* getLogDateTime() override;
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
||||
|
||||
int getInterferenceThreshold() const override {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
int getAGCResetInterval() const override {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
uint8_t getExtraAckTransmitCount() const override {
|
||||
return _prefs.multi_acks;
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||
int searchPeersByHash(const uint8_t* hash) override ;
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
|
||||
void begin(FILESYSTEM* fs);
|
||||
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
}
|
||||
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
bool formatFileSystem() override;
|
||||
void sendSelfAdvertisement(int delay_millis) override;
|
||||
void updateAdvertTimer() override;
|
||||
void updateFloodAdvertTimer() override;
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
|
||||
void eraseLogFile() override {
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
}
|
||||
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(uint8_t power_dbm) override;
|
||||
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
strcpy(reply, "not supported");
|
||||
}
|
||||
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
|
||||
static bool saveFilter(ClientInfo* client);
|
||||
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override;
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void loop();
|
||||
};
|
||||
@@ -1,925 +1,13 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <InternalFileSystem.h>
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
#include <LittleFS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <SPIFFS.h>
|
||||
#endif
|
||||
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/SimpleMeshTables.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
/* ------------------------------ Config -------------------------------- */
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "15 Jul 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.7.3"
|
||||
#endif
|
||||
|
||||
#ifndef LORA_FREQ
|
||||
#define LORA_FREQ 915.0
|
||||
#endif
|
||||
#ifndef LORA_BW
|
||||
#define LORA_BW 250
|
||||
#endif
|
||||
#ifndef LORA_SF
|
||||
#define LORA_SF 10
|
||||
#endif
|
||||
#ifndef LORA_CR
|
||||
#define LORA_CR 5
|
||||
#endif
|
||||
#ifndef LORA_TX_POWER
|
||||
#define LORA_TX_POWER 20
|
||||
#endif
|
||||
|
||||
#ifndef ADVERT_NAME
|
||||
#define ADVERT_NAME "Test BBS"
|
||||
#endif
|
||||
#ifndef ADVERT_LAT
|
||||
#define ADVERT_LAT 0.0
|
||||
#endif
|
||||
#ifndef ADVERT_LON
|
||||
#define ADVERT_LON 0.0
|
||||
#endif
|
||||
|
||||
#ifndef ADMIN_PASSWORD
|
||||
#define ADMIN_PASSWORD "password"
|
||||
#endif
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
#ifndef MAX_UNSYNCED_POSTS
|
||||
#define MAX_UNSYNCED_POSTS 32
|
||||
#endif
|
||||
|
||||
#ifndef SERVER_RESPONSE_DELAY
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
#endif
|
||||
|
||||
#ifndef TXT_ACK_DELAY
|
||||
#define TXT_ACK_DELAY 200
|
||||
#endif
|
||||
#include "MyMesh.h"
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
static UITask ui_task(display);
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "room_server"
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
/* ------------------------------ Code -------------------------------- */
|
||||
|
||||
enum RoomPermission {
|
||||
ADMIN,
|
||||
GUEST,
|
||||
READ_ONLY
|
||||
};
|
||||
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t last_timestamp; // by THEIR clock
|
||||
uint32_t last_activity; // by OUR clock
|
||||
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
|
||||
uint32_t pending_ack;
|
||||
uint32_t push_post_timestamp;
|
||||
unsigned long ack_timeout;
|
||||
RoomPermission permission;
|
||||
uint8_t push_failures;
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
int out_path_len;
|
||||
uint8_t out_path[MAX_PATH_SIZE];
|
||||
};
|
||||
|
||||
#define MAX_POST_TEXT_LEN (160-9)
|
||||
|
||||
struct PostInfo {
|
||||
mesh::Identity author;
|
||||
uint32_t post_timestamp; // by OUR clock
|
||||
char text[MAX_POST_TEXT_LEN+1];
|
||||
};
|
||||
|
||||
#define REPLY_DELAY_MILLIS 1500
|
||||
#define PUSH_NOTIFY_DELAY_MILLIS 2000
|
||||
#define SYNC_PUSH_INTERVAL 1200
|
||||
|
||||
#define PUSH_ACK_TIMEOUT_FLOOD 12000
|
||||
#define PUSH_TIMEOUT_BASE 4000
|
||||
#define PUSH_ACK_TIMEOUT_FACTOR 2000
|
||||
|
||||
#define POST_SYNC_DELAY_SECS 6
|
||||
|
||||
#define CLIENT_KEEP_ALIVE_SECS 0 // Now Disabled (was 128)
|
||||
|
||||
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||
|
||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||
|
||||
struct ServerStats {
|
||||
uint16_t batt_milli_volts;
|
||||
uint16_t curr_tx_queue_len;
|
||||
int16_t noise_floor;
|
||||
int16_t last_rssi;
|
||||
uint32_t n_packets_recv;
|
||||
uint32_t n_packets_sent;
|
||||
uint32_t total_air_time_secs;
|
||||
uint32_t total_up_time_secs;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
uint16_t err_events; // was 'n_full_events'
|
||||
int16_t last_snr; // x 4
|
||||
uint16_t n_direct_dups, n_flood_dups;
|
||||
uint16_t n_posted, n_post_push;
|
||||
};
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
FILESYSTEM* _fs;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
bool _logging;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
int num_clients;
|
||||
ClientInfo known_clients[MAX_CLIENTS];
|
||||
unsigned long next_push;
|
||||
uint16_t _num_posted, _num_post_pushes;
|
||||
int next_client_idx; // for round-robin polling
|
||||
int next_post_idx;
|
||||
PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue
|
||||
CayenneLPP telemetry;
|
||||
|
||||
ClientInfo* putClient(const mesh::Identity& id) {
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
|
||||
}
|
||||
ClientInfo* newClient;
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
newClient = &known_clients[num_clients++];
|
||||
} else { // table is currently full
|
||||
// evict least active client
|
||||
uint32_t oldest_timestamp = 0xFFFFFFFF;
|
||||
newClient = &known_clients[0];
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
auto c = &known_clients[i];
|
||||
if (c->last_activity < oldest_timestamp) {
|
||||
oldest_timestamp = c->last_activity;
|
||||
newClient = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
newClient->id = id;
|
||||
newClient->out_path_len = -1; // initially out_path is unknown
|
||||
newClient->last_timestamp = 0;
|
||||
return newClient;
|
||||
}
|
||||
|
||||
void evict(ClientInfo* client) {
|
||||
client->last_activity = 0; // this slot will now be re-used (will be oldest)
|
||||
memset(client->id.pub_key, 0, sizeof(client->id.pub_key));
|
||||
memset(client->secret, 0, sizeof(client->secret));
|
||||
client->pending_ack = 0;
|
||||
}
|
||||
|
||||
void addPost(ClientInfo* client, const char* postData) {
|
||||
// TODO: suggested postData format: <title>/<descrption>
|
||||
posts[next_post_idx].author = client->id; // add to cyclic queue
|
||||
StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN);
|
||||
|
||||
posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS;
|
||||
|
||||
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);
|
||||
_num_posted++; // stats
|
||||
}
|
||||
|
||||
void pushPostToClient(ClientInfo* client, PostInfo& post) {
|
||||
int len = 0;
|
||||
memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client
|
||||
|
||||
uint8_t attempt;
|
||||
getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different
|
||||
reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text
|
||||
|
||||
// encode prefix of post.author.pub_key
|
||||
memcpy(&reply_data[len], post.author.pub_key, 4); len += 4; // just first 4 bytes
|
||||
|
||||
int text_len = strlen(post.text);
|
||||
memcpy(&reply_data[len], post.text, text_len); len += text_len;
|
||||
|
||||
// calc expected ACK reply
|
||||
mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
|
||||
client->push_post_timestamp = post.post_timestamp;
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply);
|
||||
client->ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len);
|
||||
client->ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
|
||||
}
|
||||
_num_post_pushes++; // stats
|
||||
} else {
|
||||
client->pending_ack = 0;
|
||||
MESH_DEBUG_PRINTLN("Unable to push post to client");
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t getUnsyncedCount(ClientInfo* client) {
|
||||
uint8_t count = 0;
|
||||
for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) {
|
||||
if (posts[k].post_timestamp > client->sync_since // is new post for this Client?
|
||||
&& !posts[k].author.matches(client->id)) { // don't push posts to the author
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool processAck(const uint8_t *data) {
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
auto client = &known_clients[i];
|
||||
if (client->pending_ack && memcmp(data, &client->pending_ack, 4) == 0) { // got an ACK from Client!
|
||||
client->pending_ack = 0; // clear this, so next push can happen
|
||||
client->push_failures = 0;
|
||||
client->sync_since = client->push_post_timestamp; // advance Client's SINCE timestamp, to sync next post
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mesh::Packet* createSelfAdvert() {
|
||||
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
||||
uint8_t app_data_len;
|
||||
{
|
||||
AdvertDataBuilder builder(ADV_TYPE_ROOM, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
|
||||
app_data_len = builder.encodeTo(app_data);
|
||||
}
|
||||
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
File openAppend(const char* fname) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return _fs->open(fname, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(fname, "a");
|
||||
#else
|
||||
return _fs->open(fname, "a", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) {
|
||||
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
switch (payload[0]) {
|
||||
case REQ_TYPE_GET_STATUS: {
|
||||
ServerStats stats;
|
||||
stats.batt_milli_volts = board.getBattMilliVolts();
|
||||
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
|
||||
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
|
||||
stats.last_rssi = (int16_t) radio_driver.getLastRSSI();
|
||||
stats.n_packets_recv = radio_driver.getPacketsRecv();
|
||||
stats.n_packets_sent = radio_driver.getPacketsSent();
|
||||
stats.total_air_time_secs = getTotalAirTime() / 1000;
|
||||
stats.total_up_time_secs = _ms->getMillis() / 1000;
|
||||
stats.n_sent_flood = getNumSentFlood();
|
||||
stats.n_sent_direct = getNumSentDirect();
|
||||
stats.n_recv_flood = getNumRecvFlood();
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
stats.n_posted = _num_posted;
|
||||
stats.n_post_push = _num_post_pushes;
|
||||
|
||||
memcpy(&reply_data[4], &stats, sizeof(stats));
|
||||
return 4 + sizeof(stats);
|
||||
}
|
||||
|
||||
case REQ_TYPE_GET_TELEMETRY_DATA: {
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors(sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00, telemetry);
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
}
|
||||
return 0; // unknown command
|
||||
}
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
return _prefs.airtime_factor;
|
||||
}
|
||||
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.print(" RAW: ");
|
||||
mesh::Utils::printHex(Serial, raw, len);
|
||||
Serial.println();
|
||||
#endif
|
||||
}
|
||||
|
||||
void logRx(mesh::Packet* pkt, int len, float score) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|
||||
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void logTx(mesh::Packet* pkt, int len) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|
||||
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
|
||||
f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
|
||||
} else {
|
||||
f.printf("\n");
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
void logTxFail(mesh::Packet* pkt, int len) override {
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
if (f) {
|
||||
f.print(getLogDateTime());
|
||||
f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n",
|
||||
len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int calcRxDelay(float score, uint32_t air_time) const override {
|
||||
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
||||
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
||||
}
|
||||
|
||||
const char* getLogDateTime() override {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year());
|
||||
return tmp;
|
||||
}
|
||||
|
||||
uint32_t getRetransmitDelay(const mesh::Packet* packet) override {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6)*t;
|
||||
}
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override {
|
||||
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||
return getRNG()->nextInt(0, 6)*t;
|
||||
}
|
||||
int getInterferenceThreshold() const override {
|
||||
return _prefs.interference_threshold;
|
||||
}
|
||||
int getAGCResetInterval() const override {
|
||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
||||
uint32_t sender_timestamp, sender_sync_since;
|
||||
memcpy(&sender_timestamp, data, 4);
|
||||
memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp
|
||||
|
||||
RoomPermission perm;
|
||||
data[len] = 0; // ensure null terminator
|
||||
if (strcmp((char *) &data[8], _prefs.password) == 0) { // check for valid admin password
|
||||
perm = RoomPermission::ADMIN;
|
||||
} else {
|
||||
if (strcmp((char *) &data[8], _prefs.guest_password) == 0) { // check the room/public password
|
||||
perm = RoomPermission::GUEST;
|
||||
} else if (_prefs.allow_read_only) {
|
||||
perm = RoomPermission::READ_ONLY;
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("Incorrect room password");
|
||||
return; // no response. Client will timeout
|
||||
}
|
||||
}
|
||||
|
||||
auto client = putClient(sender); // add to known clients (if not already known)
|
||||
if (sender_timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("possible replay attack!");
|
||||
return;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->permission = perm;
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->sync_since = sender_sync_since;
|
||||
client->pending_ack = 0;
|
||||
client->push_failures = 0;
|
||||
memcpy(client->secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
client->last_activity = now;
|
||||
|
||||
now = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
// TODO: maybe reply with count of messages waiting to be synced for THIS client?
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16)
|
||||
reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2));
|
||||
reply_data[7] = getUnsyncedCount(client); // NEW
|
||||
memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed
|
||||
|
||||
next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
|
||||
int searchPeersByHash(const uint8_t* hash) override {
|
||||
int n = 0;
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (known_clients[i].id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
|
||||
int i = matching_peer_indexes[peer_idx];
|
||||
if (i >= 0 && i < num_clients) {
|
||||
// lookup pre-calculated shared_secret
|
||||
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= num_clients) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
|
||||
return;
|
||||
}
|
||||
auto client = &known_clients[i];
|
||||
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
client->last_timestamp = sender_timestamp;
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
client->last_activity = now;
|
||||
client->push_failures = 0; // reset so push can resume (if prev failed)
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
uint8_t temp[166];
|
||||
bool send_ack;
|
||||
if (flags == TXT_TYPE_CLI_DATA) {
|
||||
if (client->permission == RoomPermission::ADMIN) {
|
||||
if (is_retry) {
|
||||
temp[5] = 0; // no reply
|
||||
} else {
|
||||
handleCommand(sender_timestamp, (char *) &data[5], (char *) &temp[5]);
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN)
|
||||
}
|
||||
send_ack = false;
|
||||
} else {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // and no ACK... user shoudn't be sending these
|
||||
}
|
||||
} else { // TXT_TYPE_PLAIN
|
||||
if (client->permission == RoomPermission::READ_ONLY) {
|
||||
temp[5] = 0; // no reply
|
||||
send_ack = false; // no ACK
|
||||
} else {
|
||||
if (!is_retry) {
|
||||
addPost(client, (const char *) &data[5]);
|
||||
}
|
||||
temp[5] = 0; // no reply (ACK is enough)
|
||||
send_ack = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t delay_millis;
|
||||
if (send_ack) {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
|
||||
}
|
||||
}
|
||||
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
|
||||
} else {
|
||||
delay_millis = 0;
|
||||
}
|
||||
|
||||
int text_len = strlen((char *) &temp[5]);
|
||||
if (text_len > 0) {
|
||||
if (now == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
now++;
|
||||
}
|
||||
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
|
||||
|
||||
// calc expected ACK reply
|
||||
//mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len < 0) {
|
||||
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_REQ && len >= 5) {
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
if (sender_timestamp < client->last_timestamp) { // prevent replay attacks
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
} else {
|
||||
client->last_timestamp = sender_timestamp;
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
client->last_activity = now; // <-- THIS will keep client connection alive
|
||||
client->push_failures = 0; // reset so push can resume (if prev failed)
|
||||
|
||||
if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type
|
||||
uint32_t forceSince = 0;
|
||||
if (len >= 9) { // optional - last post_timestamp client received
|
||||
memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING!
|
||||
} else {
|
||||
memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below)
|
||||
}
|
||||
if (forceSince > 0) {
|
||||
client->sync_since = forceSince; // force-update the 'sync since'
|
||||
}
|
||||
|
||||
client->pending_ack = 0;
|
||||
|
||||
// TODO: Throttle KEEP_ALIVE requests!
|
||||
// if client sends too quickly, evict()
|
||||
|
||||
// RULE: only send keep_alive response DIRECT!
|
||||
if (client->out_path_len >= 0) {
|
||||
uint32_t ack_hash; // calc ACK to prove to sender that we got request
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
auto reply = createAck(ack_hash);
|
||||
if (reply) {
|
||||
reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4);
|
||||
if (reply_len > 0) { // valid command
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
|
||||
// TODO: prevent replay attacks
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
|
||||
if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context)
|
||||
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len);
|
||||
auto client = &known_clients[i];
|
||||
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
|
||||
}
|
||||
|
||||
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
||||
// also got an encoded ACK!
|
||||
processAck(extra);
|
||||
}
|
||||
|
||||
// NOTE: no reciprocal path send!!
|
||||
return false;
|
||||
}
|
||||
|
||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override {
|
||||
if (processAck((uint8_t *)&ack_crc)) {
|
||||
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
{
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
_logging = false;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0; // one half
|
||||
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
|
||||
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
||||
_prefs.node_lat = ADVERT_LAT;
|
||||
_prefs.node_lon = ADVERT_LON;
|
||||
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
|
||||
_prefs.freq = LORA_FREQ;
|
||||
_prefs.sf = LORA_SF;
|
||||
_prefs.bw = LORA_BW;
|
||||
_prefs.cr = LORA_CR;
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.disable_fwd = 1;
|
||||
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
|
||||
_prefs.flood_advert_interval = 3; // 3 hours
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
#ifdef ROOM_PASSWORD
|
||||
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
|
||||
#endif
|
||||
|
||||
num_clients = 0;
|
||||
next_post_idx = 0;
|
||||
next_client_idx = 0;
|
||||
next_push = 0;
|
||||
memset(posts, 0, sizeof(posts));
|
||||
_num_posted = _num_post_pushes = 0;
|
||||
}
|
||||
|
||||
void begin(FILESYSTEM* fs) {
|
||||
mesh::Mesh::begin();
|
||||
_fs = fs;
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
}
|
||||
|
||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
|
||||
const char* getRole() override { return FIRMWARE_ROLE; }
|
||||
const char* getNodeName() { return _prefs.node_name; }
|
||||
NodePrefs* getNodePrefs() {
|
||||
return &_prefs;
|
||||
}
|
||||
|
||||
void savePrefs() override {
|
||||
_cli.savePrefs(_fs);
|
||||
}
|
||||
|
||||
bool formatFileSystem() override {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return InternalFS.format();
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return LittleFS.format();
|
||||
#elif defined(ESP32)
|
||||
return SPIFFS.format();
|
||||
#else
|
||||
#error "need to implement file system erase"
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void sendSelfAdvertisement(int delay_millis) override {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
sendFlood(pkt, delay_millis);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
|
||||
}
|
||||
}
|
||||
|
||||
void updateAdvertTimer() override {
|
||||
if (_prefs.advert_interval > 0) { // schedule local advert timer
|
||||
next_local_advert = futureMillis((uint32_t)_prefs.advert_interval * 2 * 60 * 1000);
|
||||
} else {
|
||||
next_local_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
void updateFloodAdvertTimer() override {
|
||||
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
|
||||
next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
|
||||
} else {
|
||||
next_flood_advert = 0; // stop the timer
|
||||
}
|
||||
}
|
||||
|
||||
void setLoggingOn(bool enable) override { _logging = enable; }
|
||||
|
||||
void eraseLogFile() override {
|
||||
_fs->remove(PACKET_LOG_FILE);
|
||||
}
|
||||
|
||||
void dumpLogFile() override {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File f = _fs->open(PACKET_LOG_FILE, "r");
|
||||
#else
|
||||
File f = _fs->open(PACKET_LOG_FILE);
|
||||
#endif
|
||||
if (f) {
|
||||
while (f.available()) {
|
||||
int c = f.read();
|
||||
if (c < 0) break;
|
||||
Serial.print((char)c);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void setTxPower(uint8_t power_dbm) override {
|
||||
radio_set_tx_power(power_dbm);
|
||||
}
|
||||
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
strcpy(reply, "not supported");
|
||||
}
|
||||
|
||||
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
|
||||
|
||||
void clearStats() override {
|
||||
radio_driver.resetStats();
|
||||
resetStats();
|
||||
((SimpleMeshTables *)getTables())->resetStats();
|
||||
}
|
||||
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
while (*command == ' ') command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
memcpy(reply, command, 3); // reflect the prefix back
|
||||
reply += 3;
|
||||
command += 3;
|
||||
}
|
||||
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mesh::Mesh::loop();
|
||||
|
||||
if (millisHasNowPassed(next_push) && num_clients > 0) {
|
||||
// check for ACK timeouts
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
auto c = &known_clients[i];
|
||||
if (c->pending_ack && millisHasNowPassed(c->ack_timeout)) {
|
||||
c->push_failures++;
|
||||
c->pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry)
|
||||
MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures);
|
||||
}
|
||||
}
|
||||
// check next Round-Robin client, and sync next new post
|
||||
auto client = &known_clients[next_client_idx];
|
||||
bool did_push = false;
|
||||
if (client->pending_ack == 0 && client->last_activity != 0 && client->push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max
|
||||
MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t) client->id.pub_key[0]);
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) {
|
||||
auto p = &posts[idx];
|
||||
if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS && p->post_timestamp > client->sync_since // is new post for this Client?
|
||||
&& !p->author.matches(client->id)) { // don't push posts to the author
|
||||
// push this post to Client, then wait for ACK
|
||||
pushPostToClient(client, *p);
|
||||
did_push = true;
|
||||
MESH_DEBUG_PRINTLN("loop - pushed to client %02X: %s", (uint32_t) client->id.pub_key[0], p->text);
|
||||
break;
|
||||
}
|
||||
idx = (idx + 1) % MAX_UNSYNCED_POSTS; // wrap to start of cyclic queue
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t) client->id.pub_key[0]);
|
||||
}
|
||||
next_client_idx = (next_client_idx + 1) % num_clients; // round robin polling for each client
|
||||
|
||||
if (did_push) {
|
||||
next_push = futureMillis(SYNC_PUSH_INTERVAL);
|
||||
} else {
|
||||
// were no unsynced posts for curr client, so proccess next client much quicker! (in next loop())
|
||||
next_push = futureMillis(SYNC_PUSH_INTERVAL / 8);
|
||||
}
|
||||
}
|
||||
|
||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) sendFlood(pkt);
|
||||
|
||||
updateFloodAdvertTimer(); // schedule next flood advert
|
||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) sendZeroHop(pkt);
|
||||
|
||||
updateAdvertTimer(); // schedule next local advert
|
||||
}
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.loop();
|
||||
#endif
|
||||
|
||||
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
|
||||
}
|
||||
};
|
||||
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
|
||||
@@ -939,6 +27,7 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display.begin()) {
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
}
|
||||
@@ -1018,4 +107,7 @@ void loop() {
|
||||
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.loop();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -217,18 +217,18 @@ protected:
|
||||
saveContacts();
|
||||
}
|
||||
|
||||
bool processAck(const uint8_t *data) override {
|
||||
ContactInfo* processAck(const uint8_t *data) override {
|
||||
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
|
||||
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
|
||||
// NOTE: the same ACK can be received multiple times!
|
||||
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
|
||||
return true;
|
||||
return NULL; // TODO: really should return ContactInfo pointer
|
||||
}
|
||||
|
||||
//uint32_t crc;
|
||||
//memcpy(&crc, data, 4);
|
||||
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
|
||||
return false;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
|
||||
/* ------------------------------ Code -------------------------------- */
|
||||
|
||||
#define FIRMWARE_VER_LEVEL 1
|
||||
|
||||
#define REQ_TYPE_LOGIN 0x00
|
||||
#define REQ_TYPE_GET_STATUS 0x01
|
||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||
@@ -71,78 +73,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
_fs->remove(filename);
|
||||
return _fs->open(filename, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(filename, "w");
|
||||
#else
|
||||
return _fs->open(filename, "w", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SensorMesh::loadContacts() {
|
||||
num_contacts = 0;
|
||||
if (_fs->exists("/s_contacts")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = _fs->open("/s_contacts", "r");
|
||||
#else
|
||||
File file = _fs->open("/s_contacts");
|
||||
#endif
|
||||
if (file) {
|
||||
bool full = false;
|
||||
while (!full) {
|
||||
ContactInfo c;
|
||||
uint8_t pub_key[32];
|
||||
uint8_t unused[6];
|
||||
|
||||
bool success = (file.read(pub_key, 32) == 32);
|
||||
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
|
||||
success = success && (file.read(unused, 6) == 6);
|
||||
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.read(c.out_path, 64) == 64);
|
||||
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
c.last_timestamp = 0; // transient
|
||||
c.last_activity = 0;
|
||||
|
||||
if (!success) break; // EOF
|
||||
|
||||
c.id = mesh::Identity(pub_key);
|
||||
if (num_contacts < MAX_CONTACTS) {
|
||||
contacts[num_contacts++] = c;
|
||||
} else {
|
||||
full = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SensorMesh::saveContacts() {
|
||||
File file = openWrite(_fs, "/s_contacts");
|
||||
if (file) {
|
||||
uint8_t unused[5];
|
||||
memset(unused, 0, sizeof(unused));
|
||||
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
auto c = &contacts[i];
|
||||
if (c->permissions == 0) continue; // skip deleted entries
|
||||
|
||||
bool success = (file.write(c->id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
|
||||
success = success && (file.write(unused, 6) == 6);
|
||||
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
|
||||
success = success && (file.write(c->out_path, 64) == 64);
|
||||
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
|
||||
if (!success) break; // write failed
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t getDataSize(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
@@ -243,18 +173,20 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult
|
||||
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
|
||||
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) {
|
||||
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
|
||||
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
// query other sensors -- target specific
|
||||
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest
|
||||
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
||||
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_OTHER_STATS) != 0) {
|
||||
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||
uint32_t start_secs_ago, end_secs_ago;
|
||||
memcpy(&start_secs_ago, &payload[0], 4);
|
||||
memcpy(&end_secs_ago, &payload[4], 4);
|
||||
@@ -288,13 +220,14 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
||||
}
|
||||
return ofs;
|
||||
}
|
||||
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3) {
|
||||
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
|
||||
uint8_t res1 = payload[0]; // reserved for future (extra query params)
|
||||
uint8_t res2 = payload[1];
|
||||
if (res1 == 0 && res2 == 0) {
|
||||
uint8_t ofs = 4;
|
||||
for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
||||
auto c = &contacts[i];
|
||||
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted entries
|
||||
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
|
||||
reply_data[ofs++] = c->permissions;
|
||||
}
|
||||
@@ -315,43 +248,7 @@ mesh::Packet* SensorMesh::createSelfAdvert() {
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
ContactInfo* SensorMesh::putContact(const mesh::Identity& id) {
|
||||
uint32_t min_time = 0xFFFFFFFF;
|
||||
ContactInfo* oldest = &contacts[MAX_CONTACTS - 1];
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
if (id.matches(contacts[i].id)) return &contacts[i]; // already known
|
||||
if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) {
|
||||
oldest = &contacts[i];
|
||||
min_time = oldest->last_activity;
|
||||
}
|
||||
}
|
||||
|
||||
ContactInfo* c;
|
||||
if (num_contacts < MAX_CONTACTS) {
|
||||
c = &contacts[num_contacts++];
|
||||
} else {
|
||||
c = oldest; // evict least active contact
|
||||
}
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->id = id;
|
||||
c->out_path_len = -1; // initially out_path is unknown
|
||||
return c;
|
||||
}
|
||||
|
||||
void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint8_t perms) {
|
||||
mesh::Identity id(pubkey);
|
||||
auto c = putContact(id);
|
||||
|
||||
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
|
||||
memset(c, 0, sizeof(*c));
|
||||
} else {
|
||||
c->permissions = perms; // update their permissions
|
||||
self_id.calcSharedSecret(c->shared_secret, pubkey);
|
||||
}
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts()
|
||||
}
|
||||
|
||||
void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) {
|
||||
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
|
||||
int text_len = strlen(t->text);
|
||||
|
||||
uint8_t data[MAX_PACKET_PAYLOAD];
|
||||
@@ -434,36 +331,48 @@ int SensorMesh::getAGCResetInterval() const {
|
||||
}
|
||||
|
||||
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
|
||||
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid password
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
||||
#endif
|
||||
return 0;
|
||||
ClientInfo* client;
|
||||
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
||||
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
|
||||
if (client == NULL) {
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
||||
#if MESH_DEBUG
|
||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
|
||||
if (sender_timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
||||
return 0; // FATAL: client table is full -OR- replay attack
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->permissions |= PERM_ACL_ADMIN;
|
||||
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
|
||||
auto client = putContact(sender); // add to contacts (if not already known)
|
||||
if (sender_timestamp <= client->last_timestamp) {
|
||||
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
||||
return 0; // FATAL: client table is full -OR- replay attack
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("Login success!");
|
||||
client->last_timestamp = sender_timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
client->permissions = PERM_ACL_LEVEL3 | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO; // initially opt-in to receive alerts (can opt out)
|
||||
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
|
||||
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
|
||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
||||
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
|
||||
reply_data[6] = 1; // 1 = is admin
|
||||
reply_data[5] = 0;
|
||||
reply_data[6] = client->isAdmin() ? 1 : 0;
|
||||
reply_data[7] = client->permissions;
|
||||
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
||||
reply_data[12] = FIRMWARE_VER_LEVEL;
|
||||
|
||||
return 12; // reply length
|
||||
return 13; // reply length
|
||||
}
|
||||
|
||||
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
@@ -484,31 +393,56 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
||||
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
||||
char* hex = &command[8];
|
||||
char* sp = strchr(hex, ' '); // look for separator char
|
||||
if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) {
|
||||
strcpy(reply, "Err - bad pubkey len");
|
||||
if (sp == NULL) {
|
||||
strcpy(reply, "Err - bad params");
|
||||
} else {
|
||||
*sp++ = 0; // replace space with null terminator
|
||||
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) {
|
||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||
uint8_t perms = atoi(sp);
|
||||
applyContactPermissions(pubkey, perms);
|
||||
strcpy(reply, "OK");
|
||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - invalid params");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Err - bad pubkey");
|
||||
}
|
||||
}
|
||||
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
|
||||
Serial.println("ACL:");
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
auto c = &contacts[i];
|
||||
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted entries
|
||||
|
||||
Serial.printf("%02X ", c->permissions);
|
||||
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf("\n");
|
||||
}
|
||||
reply[0] = 0;
|
||||
} else {
|
||||
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
|
||||
if (command[2] == ' ') { // it's a write
|
||||
uint32_t val;
|
||||
uint32_t g = board.getGpio();
|
||||
if (command[3] == 'r') { // reset bits
|
||||
sscanf(&command[4], "%x", &val);
|
||||
val = g & ~val;
|
||||
} else if (command[3] == 's') { // set bits
|
||||
sscanf(&command[4], "%x", &val);
|
||||
val |= g;
|
||||
} else if (command[3] == 't') { // toggle bits
|
||||
sscanf(&command[4], "%x", &val);
|
||||
val ^= g;
|
||||
} else { // set value
|
||||
sscanf(&command[3], "%x", &val);
|
||||
}
|
||||
board.setGpio(val);
|
||||
}
|
||||
sprintf(reply, "%x", board.getGpio());
|
||||
} else{
|
||||
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
||||
}
|
||||
}
|
||||
@@ -537,8 +471,8 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
|
||||
|
||||
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
|
||||
if (contacts[i].id.isHashMatch(hash)) {
|
||||
for (int i = 0; i < acl.getNumClients() && n < MAX_SEARCH_RESULTS; i++) {
|
||||
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
}
|
||||
@@ -547,44 +481,61 @@ int SensorMesh::searchPeersByHash(const uint8_t* hash) {
|
||||
|
||||
void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
|
||||
int i = matching_peer_indexes[peer_idx];
|
||||
if (i >= 0 && i < num_contacts) {
|
||||
if (i >= 0 && i < acl.getNumClients()) {
|
||||
// lookup pre-calculated shared_secret
|
||||
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
|
||||
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) {
|
||||
if (dest.out_path_len < 0) {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
|
||||
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet* a2 = createAck(ack_hash);
|
||||
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
|
||||
}
|
||||
}
|
||||
|
||||
void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= num_contacts) {
|
||||
if (i < 0 || i >= acl.getNumClients()) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
|
||||
return;
|
||||
}
|
||||
|
||||
ContactInfo& from = contacts[i];
|
||||
ClientInfo* from = acl.getClientByIdx(i);
|
||||
|
||||
if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact)
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
if (timestamp > from.last_timestamp) { // prevent replay attacks
|
||||
uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFFFF : from.permissions, timestamp, data[4], &data[5], len - 5);
|
||||
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
||||
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
|
||||
if (reply_len == 0) return; // invalid command
|
||||
|
||||
from.last_timestamp = timestamp;
|
||||
from.last_activity = getRTCClock()->getCurrentTime();
|
||||
from->last_timestamp = timestamp;
|
||||
from->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
|
||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, reply_data, reply_len);
|
||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
||||
if (reply) {
|
||||
if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
|
||||
if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
|
||||
} else {
|
||||
sendFlood(reply, SERVER_RESPONSE_DELAY);
|
||||
}
|
||||
@@ -593,43 +544,60 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) { // a CLI command
|
||||
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from->isAdmin()) { // a CLI command
|
||||
uint32_t sender_timestamp;
|
||||
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||
uint flags = (data[4] >> 2); // message attempt number, and other flags
|
||||
|
||||
if (!(flags == TXT_TYPE_CLI_DATA)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp > from.last_timestamp) { // prevent replay attacks
|
||||
from.last_timestamp = sender_timestamp;
|
||||
from.last_activity = getRTCClock()->getCurrentTime();
|
||||
if (sender_timestamp > from->last_timestamp) { // prevent replay attacks
|
||||
if (flags == TXT_TYPE_PLAIN) {
|
||||
bool handled = handleIncomingMsg(*from, sender_timestamp, &data[5], flags, len - 5);
|
||||
if (handled) { // if msg was handled then send an ack
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from->id.pub_key, PUB_KEY_SIZE);
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
uint8_t temp[166];
|
||||
char *command = (char *) &data[5];
|
||||
char *reply = (char *) &temp[5];
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
if (timestamp == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
timestamp++;
|
||||
}
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2);
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (from.out_path_len < 0) {
|
||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
if (path) sendFlood(path, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
sendAckTo(*from, ack_hash);
|
||||
}
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
from->last_timestamp = sender_timestamp;
|
||||
from->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
// len can be > original length, but 'text' will be padded with zeroes
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
uint8_t temp[166];
|
||||
char *command = (char *) &data[5];
|
||||
char *reply = (char *) &temp[5];
|
||||
handleCommand(sender_timestamp, command, reply);
|
||||
|
||||
int text_len = strlen(reply);
|
||||
if (text_len > 0) {
|
||||
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
if (timestamp == sender_timestamp) {
|
||||
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
||||
timestamp++;
|
||||
}
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = (TXT_TYPE_CLI_DATA << 2);
|
||||
|
||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
||||
if (reply) {
|
||||
if (from->out_path_len < 0) {
|
||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
|
||||
} else {
|
||||
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
@@ -637,23 +605,32 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
}
|
||||
}
|
||||
|
||||
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len) {
|
||||
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
|
||||
#ifdef MESH_DEBUG
|
||||
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
|
||||
Serial.printf(": %s\n", data);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||
int i = matching_peer_indexes[sender_idx];
|
||||
if (i < 0 || i >= num_contacts) {
|
||||
if (i < 0 || i >= acl.getNumClients()) {
|
||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
|
||||
return false;
|
||||
}
|
||||
|
||||
ContactInfo& from = contacts[i];
|
||||
ClientInfo* from = acl.getClientByIdx(i);
|
||||
|
||||
MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
|
||||
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
|
||||
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
|
||||
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
from.last_activity = getRTCClock()->getCurrentTime();
|
||||
memcpy(from->out_path, path, from->out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
from->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
|
||||
if (from.isAdmin()) {
|
||||
if (from->isAdmin()) {
|
||||
// only do saveContacts() (of this out_path change) if this is an admin
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
}
|
||||
@@ -680,11 +657,11 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
{
|
||||
num_contacts = 0;
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
last_read_time = 0;
|
||||
num_alert_tasks = 0;
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
@@ -713,7 +690,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
|
||||
loadContacts();
|
||||
acl.load(_fs);
|
||||
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
@@ -735,6 +712,30 @@ bool SensorMesh::formatFileSystem() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
|
||||
self_id = new_id;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
IdentityStore store(*_fs, "");
|
||||
#elif defined(ESP32)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
IdentityStore store(*_fs, "/identity");
|
||||
#else
|
||||
#error "need to define saveIdentity()"
|
||||
#endif
|
||||
store.save("_main", self_id);
|
||||
}
|
||||
|
||||
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
||||
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
|
||||
pending_freq = freq;
|
||||
pending_bw = bw;
|
||||
pending_sf = sf;
|
||||
pending_cr = cr;
|
||||
|
||||
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
|
||||
}
|
||||
|
||||
void SensorMesh::sendSelfAdvertisement(int delay_millis) {
|
||||
mesh::Packet* pkt = createSelfAdvert();
|
||||
if (pkt) {
|
||||
@@ -810,6 +811,18 @@ void SensorMesh::loop() {
|
||||
updateAdvertTimer(); // schedule next local advert
|
||||
}
|
||||
|
||||
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
||||
set_radio_at = 0; // clear timer
|
||||
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
|
||||
MESH_DEBUG_PRINTLN("Temp radio params");
|
||||
}
|
||||
|
||||
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
||||
revert_radio_at = 0; // clear timer
|
||||
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
uint32_t curr = getRTCClock()->getCurrentTime();
|
||||
if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
|
||||
telemetry.reset();
|
||||
@@ -829,13 +842,13 @@ void SensorMesh::loop() {
|
||||
if (millisHasNowPassed(t->send_expiry)) { // next send needed?
|
||||
if (t->attempt >= 4) { // max attempts reached, try next contact
|
||||
t->curr_contact_idx++;
|
||||
if (t->curr_contact_idx >= num_contacts) { // no more contacts to try?
|
||||
if (t->curr_contact_idx >= acl.getNumClients()) { // no more contacts to try?
|
||||
num_alert_tasks--; // remove t from queue
|
||||
for (int i = 0; i < num_alert_tasks; i++) {
|
||||
alert_tasks[i] = alert_tasks[i + 1];
|
||||
}
|
||||
} else {
|
||||
auto c = &contacts[t->curr_contact_idx];
|
||||
auto c = acl.getClientByIdx(t->curr_contact_idx);
|
||||
uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
|
||||
|
||||
if (c->permissions & pri_mask) { // contact wants alert
|
||||
@@ -848,8 +861,8 @@ void SensorMesh::loop() {
|
||||
// next contact tested in next ::loop()
|
||||
}
|
||||
}
|
||||
} else if (t->curr_contact_idx < num_contacts) {
|
||||
auto c = &contacts[t->curr_contact_idx]; // send next attempt
|
||||
} else if (t->curr_contact_idx < acl.getNumClients()) {
|
||||
auto c = acl.getClientByIdx(t->curr_contact_idx); // send next attempt
|
||||
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
|
||||
} else {
|
||||
// contact list has likely been modified while waiting for alert ACK, cancel this task
|
||||
@@ -860,7 +873,7 @@ void SensorMesh::loop() {
|
||||
|
||||
// is there are pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
saveContacts();
|
||||
acl.save(_fs);
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,46 +20,27 @@
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <RTClib.h>
|
||||
#include <target.h>
|
||||
|
||||
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
|
||||
#define PERM_ACL_GUEST 0
|
||||
#define PERM_ACL_LEVEL1 1
|
||||
#define PERM_ACL_LEVEL2 2
|
||||
#define PERM_ACL_LEVEL3 3 // admin
|
||||
|
||||
#define PERM_GET_TELEMETRY (1 << 2)
|
||||
#define PERM_GET_OTHER_STATS (1 << 3)
|
||||
#define PERM_RESERVED1 (1 << 4)
|
||||
#define PERM_RESERVED2 (1 << 5)
|
||||
#define PERM_RESERVED1 (1 << 2)
|
||||
#define PERM_RESERVED2 (1 << 3)
|
||||
#define PERM_RESERVED3 (1 << 4)
|
||||
#define PERM_RESERVED4 (1 << 5)
|
||||
#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts
|
||||
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
|
||||
|
||||
struct ContactInfo {
|
||||
mesh::Identity id;
|
||||
uint8_t permissions;
|
||||
int8_t out_path_len;
|
||||
uint8_t out_path[MAX_PATH_SIZE];
|
||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
uint32_t last_timestamp; // by THEIR clock (transient)
|
||||
uint32_t last_activity; // by OUR clock (transient)
|
||||
|
||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3; }
|
||||
};
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "2 Jul 2025"
|
||||
#define FIRMWARE_BUILD_DATE "28 Sep 2025"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "v1.7.2"
|
||||
#define FIRMWARE_VERSION "v1.9.0"
|
||||
#endif
|
||||
|
||||
#define FIRMWARE_ROLE "sensor"
|
||||
|
||||
#define MAX_CONTACTS 20
|
||||
|
||||
#define MAX_SEARCH_RESULTS 8
|
||||
#define MAX_CONCURRENT_ALERTS 4
|
||||
|
||||
@@ -88,8 +69,10 @@ public:
|
||||
void formatNeighborsReply(char *reply) override {
|
||||
strcpy(reply, "not supported");
|
||||
}
|
||||
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||
void clearStats() override { }
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
|
||||
float getTelemValue(uint8_t channel, uint8_t type);
|
||||
|
||||
@@ -139,30 +122,31 @@ protected:
|
||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
|
||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
||||
|
||||
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
|
||||
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
|
||||
private:
|
||||
FILESYSTEM* _fs;
|
||||
unsigned long next_local_advert, next_flood_advert;
|
||||
NodePrefs _prefs;
|
||||
CommonCLI _cli;
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||
ContactInfo contacts[MAX_CONTACTS];
|
||||
int num_contacts;
|
||||
ClientACL acl;
|
||||
unsigned long dirty_contacts_expiry;
|
||||
CayenneLPP telemetry;
|
||||
uint32_t last_read_time;
|
||||
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
||||
int num_alert_tasks;
|
||||
Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
uint8_t pending_cr;
|
||||
|
||||
void loadContacts();
|
||||
void saveContacts();
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
|
||||
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
|
||||
mesh::Packet* createSelfAdvert();
|
||||
ContactInfo* putContact(const mesh::Identity& id);
|
||||
void applyContactPermissions(const uint8_t* pubkey, uint8_t perms);
|
||||
|
||||
void sendAlert(ContactInfo* c, Trigger* t);
|
||||
void sendAlert(const ClientInfo* c, Trigger* t);
|
||||
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ void halt() {
|
||||
while (1) ;
|
||||
}
|
||||
|
||||
static char command[120];
|
||||
static char command[160];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
16
library.json
Normal file
16
library.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "MeshCore",
|
||||
"version" : "1.8.0",
|
||||
"dependencies": {
|
||||
"SPI": "*",
|
||||
"Wire": "*",
|
||||
"jgromes/RadioLib": "^7.1.2",
|
||||
"rweather/Crypto": "^0.4.0",
|
||||
"adafruit/RTClib": "^2.1.3",
|
||||
"melopero/Melopero RV3028": "^1.1.0",
|
||||
"electroniccats/CayenneLPP": "1.4.0"
|
||||
},
|
||||
"build": {
|
||||
"extraScript": "build_as_lib.py"
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,15 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
|
||||
build_src_filter =
|
||||
+<*.cpp>
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/radiolib/*.cpp>
|
||||
+<helpers/bridges/BridgeBase.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
|
||||
; ----------------- ESP32 ---------------------
|
||||
|
||||
[esp32_base]
|
||||
extends = arduino_base
|
||||
platform = platformio/espressif32@^6.11.0
|
||||
platform = platformio/espressif32@6.11.0
|
||||
monitor_filters = esp32_exception_decoder
|
||||
extra_scripts = merge-bin.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
@@ -66,7 +69,7 @@ lib_deps =
|
||||
; esp32c6 uses arduino framework 3.x
|
||||
[esp32c6_base]
|
||||
extends = esp32_base
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
|
||||
|
||||
; ----------------- NRF52 ---------------------
|
||||
|
||||
@@ -76,7 +79,10 @@ platform = nordicnrf52
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-D LFS_NO_ASSERT=1
|
||||
|
||||
-D EXTRAFS=1
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
https://github.com/oltaco/CustomLFS @ 0.2.1
|
||||
; ----------------- RP2040 ---------------------
|
||||
|
||||
[rp2040_base]
|
||||
@@ -101,3 +107,34 @@ build_src_filter = ${arduino_base.build_src_filter}
|
||||
+<helpers/stm32>
|
||||
lib_deps = ${arduino_base.lib_deps}
|
||||
file://arch/stm32/Adafruit_LittleFS_stm32
|
||||
adafruit/Adafruit BusIO @ 1.17.2
|
||||
|
||||
[sensor_base]
|
||||
build_flags =
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
-D ENV_INCLUDE_AHTX0=1
|
||||
-D ENV_INCLUDE_BME280=1
|
||||
-D ENV_INCLUDE_BMP280=1
|
||||
-D ENV_INCLUDE_SHTC3=1
|
||||
-D ENV_INCLUDE_SHT4X=1
|
||||
-D ENV_INCLUDE_LPS22HB=1
|
||||
-D ENV_INCLUDE_INA3221=1
|
||||
-D ENV_INCLUDE_INA219=1
|
||||
-D ENV_INCLUDE_INA226=1
|
||||
-D ENV_INCLUDE_INA260=1
|
||||
-D ENV_INCLUDE_MLX90614=1
|
||||
-D ENV_INCLUDE_VL53L0X=1
|
||||
lib_deps =
|
||||
adafruit/Adafruit INA3221 Library @ ^1.0.1
|
||||
adafruit/Adafruit INA219 @ ^1.2.3
|
||||
robtillaart/INA226 @ ^0.6.4
|
||||
adafruit/Adafruit INA260 Library @ ^1.5.3
|
||||
adafruit/Adafruit AHTX0 @ ^2.0.5
|
||||
adafruit/Adafruit BME280 Library @ ^2.3.0
|
||||
adafruit/Adafruit BMP280 Library @ ^2.6.8
|
||||
adafruit/Adafruit SHTC3 Library @ ^1.0.1
|
||||
sensirion/Sensirion I2C SHT4x @ ^1.1.2
|
||||
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
|
||||
adafruit/Adafruit MLX90614 Library @ ^2.1.5
|
||||
adafruit/Adafruit_VL53L0X @ ^1.2.4
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
|
||||
@@ -159,6 +159,7 @@ void Dispatcher::checkRecv() {
|
||||
pkt->_snr = _radio->getLastSNR() * 4.0f;
|
||||
score = _radio->packetScore(_radio->getLastSNR(), len);
|
||||
air_time = _radio->getEstAirtimeFor(len);
|
||||
rx_air_time += air_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,9 +170,9 @@ void Dispatcher::checkRecv() {
|
||||
if (pkt) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.print(getLogDateTime());
|
||||
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
|
||||
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
|
||||
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
|
||||
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
|
||||
|
||||
static uint8_t packet_hash[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(packet_hash);
|
||||
|
||||
@@ -114,7 +114,7 @@ typedef uint32_t DispatcherAction;
|
||||
*/
|
||||
class Dispatcher {
|
||||
Packet* outbound; // current outbound packet
|
||||
unsigned long outbound_expiry, outbound_start, total_air_time;
|
||||
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
|
||||
unsigned long next_tx_time;
|
||||
unsigned long cad_busy_start;
|
||||
unsigned long radio_nonrx_start;
|
||||
@@ -134,7 +134,9 @@ protected:
|
||||
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
|
||||
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
||||
{
|
||||
outbound = NULL; total_air_time = 0; next_tx_time = 0;
|
||||
outbound = NULL;
|
||||
total_air_time = rx_air_time = 0;
|
||||
next_tx_time = 0;
|
||||
cad_busy_start = 0;
|
||||
next_floor_calib_time = next_agc_reset_time = 0;
|
||||
_err_flags = 0;
|
||||
@@ -167,6 +169,7 @@ public:
|
||||
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
|
||||
|
||||
unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds
|
||||
unsigned long getReceiveAirTime() const {return rx_air_time; }
|
||||
uint32_t getNumSentFlood() const { return n_sent_flood; }
|
||||
uint32_t getNumSentDirect() const { return n_sent_direct; }
|
||||
uint32_t getNumRecvFlood() const { return n_recv_flood; }
|
||||
|
||||
@@ -92,7 +92,7 @@ void LocalIdentity::sign(uint8_t* sig, const uint8_t* message, int msg_len) cons
|
||||
ed25519_sign(sig, message, msg_len, pub_key, prv_key);
|
||||
}
|
||||
|
||||
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) {
|
||||
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const {
|
||||
ed25519_key_exchange(secret, other_pub_key, prv_key);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,14 +64,14 @@ public:
|
||||
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
|
||||
* \param other IN - the second party in the exchange.
|
||||
*/
|
||||
void calcSharedSecret(uint8_t* secret, const Identity& other) { calcSharedSecret(secret, other.pub_key); }
|
||||
void calcSharedSecret(uint8_t* secret, const Identity& other) const { calcSharedSecret(secret, other.pub_key); }
|
||||
|
||||
/**
|
||||
* \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519.
|
||||
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
|
||||
* \param other_pub_key IN - the public key of second party in the exchange (must be PUB_KEY_SIZE bytes)
|
||||
*/
|
||||
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key);
|
||||
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
|
||||
|
||||
bool readFrom(Stream& s);
|
||||
bool writeTo(Stream& s) const;
|
||||
|
||||
134
src/Mesh.cpp
134
src/Mesh.cpp
@@ -22,6 +22,9 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
|
||||
uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) {
|
||||
return 0; // by default, no delay
|
||||
}
|
||||
uint8_t Mesh::getExtraAckTransmitCount() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t Mesh::getCADFailRetryDelay() const {
|
||||
return _rng->nextInt(1, 4)*120;
|
||||
@@ -67,22 +70,22 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
|
||||
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
|
||||
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
|
||||
if (_tables->hasSeen(pkt)) return ACTION_RELEASE; // don't retransmit!
|
||||
|
||||
// remove our hash from 'path', then re-broadcast
|
||||
pkt->path_len -= PATH_HASH_SIZE;
|
||||
#if 0
|
||||
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
|
||||
#elif PATH_HASH_SIZE == 1
|
||||
for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
|
||||
pkt->path[k] = pkt->path[k + 1];
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
|
||||
return forwardMultipartDirect(pkt);
|
||||
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
|
||||
if (!_tables->hasSeen(pkt)) { // don't retransmit!
|
||||
removeSelfFromPath(pkt);
|
||||
routeDirectRecvAcks(pkt, 0);
|
||||
}
|
||||
return ACTION_RELEASE;
|
||||
}
|
||||
#else
|
||||
#error "need path remove impl"
|
||||
#endif
|
||||
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
|
||||
if (!_tables->hasSeen(pkt)) {
|
||||
removeSelfFromPath(pkt);
|
||||
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
|
||||
}
|
||||
}
|
||||
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
|
||||
}
|
||||
@@ -261,6 +264,32 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PAYLOAD_TYPE_MULTIPART:
|
||||
if (pkt->payload_len > 2) {
|
||||
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
|
||||
uint8_t type = pkt->payload[0] & 0x0F;
|
||||
|
||||
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
|
||||
Packet tmp;
|
||||
tmp.header = pkt->header;
|
||||
tmp.path_len = pkt->path_len;
|
||||
memcpy(tmp.path, pkt->path, pkt->path_len);
|
||||
tmp.payload_len = pkt->payload_len - 1;
|
||||
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
|
||||
|
||||
if (!_tables->hasSeen(&tmp)) {
|
||||
uint32_t ack_crc;
|
||||
memcpy(&ack_crc, tmp.payload, 4);
|
||||
|
||||
onAckRecv(&tmp, ack_crc);
|
||||
//action = routeRecvPacket(&tmp); // NOTE: currently not needed, as multipart ACKs not sent Flood
|
||||
}
|
||||
} else {
|
||||
// FUTURE: other multipart types??
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header);
|
||||
// Don't flood route unknown packet types! action = routeRecvPacket(pkt);
|
||||
@@ -269,6 +298,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
return action;
|
||||
}
|
||||
|
||||
void Mesh::removeSelfFromPath(Packet* pkt) {
|
||||
// remove our hash from 'path'
|
||||
pkt->path_len -= PATH_HASH_SIZE;
|
||||
#if 0
|
||||
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
|
||||
#elif PATH_HASH_SIZE == 1
|
||||
for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1
|
||||
pkt->path[k] = pkt->path[k + 1];
|
||||
}
|
||||
#else
|
||||
#error "need path remove impl"
|
||||
#endif
|
||||
}
|
||||
|
||||
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
|
||||
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
|
||||
&& packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
|
||||
@@ -282,6 +325,54 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
|
||||
return ACTION_RELEASE;
|
||||
}
|
||||
|
||||
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
|
||||
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
|
||||
uint8_t type = pkt->payload[0] & 0x0F;
|
||||
|
||||
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
|
||||
Packet tmp;
|
||||
tmp.header = pkt->header;
|
||||
tmp.path_len = pkt->path_len;
|
||||
memcpy(tmp.path, pkt->path, pkt->path_len);
|
||||
tmp.payload_len = pkt->payload_len - 1;
|
||||
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
|
||||
|
||||
if (!_tables->hasSeen(&tmp)) { // don't retransmit!
|
||||
removeSelfFromPath(&tmp);
|
||||
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
|
||||
}
|
||||
}
|
||||
return ACTION_RELEASE;
|
||||
}
|
||||
|
||||
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
|
||||
if (!packet->isMarkedDoNotRetransmit()) {
|
||||
uint32_t crc;
|
||||
memcpy(&crc, packet->payload, 4);
|
||||
|
||||
uint8_t extra = getExtraAckTransmitCount();
|
||||
while (extra > 0) {
|
||||
delay_millis += getDirectRetransmitDelay(packet) + 300;
|
||||
auto a1 = createMultiAck(crc, extra);
|
||||
if (a1) {
|
||||
memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
|
||||
a1->header &= ~PH_ROUTE_MASK;
|
||||
a1->header |= ROUTE_TYPE_DIRECT;
|
||||
sendPacket(a1, 0, delay_millis);
|
||||
}
|
||||
extra--;
|
||||
}
|
||||
|
||||
auto a2 = createAck(crc);
|
||||
if (a2) {
|
||||
memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
|
||||
a2->header &= ~PH_ROUTE_MASK;
|
||||
a2->header |= ROUTE_TYPE_DIRECT;
|
||||
sendPacket(a2, 0, delay_millis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
|
||||
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
|
||||
|
||||
@@ -449,6 +540,21 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
|
||||
Packet* packet = obtainNewPacket();
|
||||
if (packet == NULL) {
|
||||
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
|
||||
return NULL;
|
||||
}
|
||||
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
|
||||
|
||||
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
|
||||
memcpy(&packet->payload[1], &ack_crc, 4);
|
||||
packet->payload_len = 5;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
|
||||
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
|
||||
|
||||
|
||||
11
src/Mesh.h
11
src/Mesh.h
@@ -28,6 +28,11 @@ class Mesh : public Dispatcher {
|
||||
RNG* _rng;
|
||||
MeshTables* _tables;
|
||||
|
||||
void removeSelfFromPath(Packet* packet);
|
||||
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
|
||||
DispatcherAction forwardMultipartDirect(Packet* pkt);
|
||||
|
||||
protected:
|
||||
DispatcherAction onRecvPacket(Packet* pkt) override;
|
||||
|
||||
@@ -54,6 +59,11 @@ protected:
|
||||
*/
|
||||
virtual uint32_t getDirectRetransmitDelay(const Packet* packet);
|
||||
|
||||
/**
|
||||
* \returns number of extra (Direct) ACK transmissions wanted.
|
||||
*/
|
||||
virtual uint8_t getExtraAckTransmitCount() const;
|
||||
|
||||
/**
|
||||
* \brief Perform search of local DB of peers/contacts.
|
||||
* \returns Number of peers with matching hash
|
||||
@@ -165,6 +175,7 @@ public:
|
||||
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
|
||||
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
|
||||
Packet* createAck(uint32_t ack_crc);
|
||||
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
|
||||
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
|
||||
Packet* createRawData(const uint8_t* data, size_t len);
|
||||
|
||||
@@ -41,6 +41,8 @@ public:
|
||||
virtual void onAfterTransmit() { }
|
||||
virtual void reboot() = 0;
|
||||
virtual void powerOff() { /* no op */ }
|
||||
virtual uint32_t getGpio() { return 0; }
|
||||
virtual void setGpio(uint32_t values) {}
|
||||
virtual uint8_t getStartupReason() const = 0;
|
||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace mesh {
|
||||
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||
#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop
|
||||
#define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets
|
||||
//...
|
||||
#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||
|
||||
|
||||
34
src/helpers/AbstractBridge.h
Normal file
34
src/helpers/AbstractBridge.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <Mesh.h>
|
||||
|
||||
class AbstractBridge {
|
||||
public:
|
||||
virtual ~AbstractBridge() {}
|
||||
|
||||
/**
|
||||
* @brief Initializes the bridge.
|
||||
*/
|
||||
virtual void begin() = 0;
|
||||
|
||||
/**
|
||||
* @brief A method to be called on every main loop iteration.
|
||||
* Used for tasks like checking for incoming data.
|
||||
*/
|
||||
virtual void loop() = 0;
|
||||
|
||||
/**
|
||||
* @brief A callback that is triggered when the mesh transmits a packet.
|
||||
* The bridge can use this to forward the packet.
|
||||
*
|
||||
* @param packet The packet that was transmitted.
|
||||
*/
|
||||
virtual void onPacketTransmitted(mesh::Packet* packet) = 0;
|
||||
|
||||
/**
|
||||
* @brief Processes a received packet from the bridge's medium.
|
||||
*
|
||||
* @param packet The packet that was received.
|
||||
*/
|
||||
virtual void onPacketReceived(mesh::Packet* packet) = 0;
|
||||
};
|
||||
@@ -31,6 +31,23 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
|
||||
return createAdvert(self_id, app_data, app_data_len);
|
||||
}
|
||||
|
||||
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
|
||||
if (dest.out_path_len < 0) {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
uint32_t d = TXT_ACK_DELAY;
|
||||
if (getExtraAckTransmitCount() > 0) {
|
||||
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
|
||||
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
|
||||
d += 300;
|
||||
}
|
||||
|
||||
mesh::Packet* a2 = createAck(ack_hash);
|
||||
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
|
||||
AdvertDataParser parser(app_data, app_data_len);
|
||||
if (!(parser.isValid() && parser.hasName())) {
|
||||
@@ -141,6 +158,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
data[len] = 0; // need to make a C string again, with null terminator
|
||||
|
||||
if (flags == TXT_TYPE_PLAIN) {
|
||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
||||
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||
@@ -152,14 +170,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
if (path) sendFlood(path, TXT_ACK_DELAY);
|
||||
} else {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) {
|
||||
if (from.out_path_len < 0) {
|
||||
sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
|
||||
}
|
||||
}
|
||||
sendAckTo(from, ack_hash);
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
|
||||
@@ -174,6 +185,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
|
||||
from.sync_since = timestamp;
|
||||
}
|
||||
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
|
||||
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
|
||||
|
||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
|
||||
@@ -185,14 +197,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||
if (path) sendFlood(path, TXT_ACK_DELAY);
|
||||
} else {
|
||||
mesh::Packet* ack = createAck(ack_hash);
|
||||
if (ack) {
|
||||
if (from.out_path_len < 0) {
|
||||
sendFlood(ack, TXT_ACK_DELAY);
|
||||
} else {
|
||||
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
|
||||
}
|
||||
}
|
||||
sendAckTo(from, ack_hash);
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
|
||||
@@ -220,6 +225,10 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
|
||||
}
|
||||
} else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
|
||||
onContactResponse(from, data, len);
|
||||
if (packet->isRouteFlood() && from.out_path_len >= 0) {
|
||||
// we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?)
|
||||
handleReturnPathRetry(from, packet->path, packet->path_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,16 +241,20 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
|
||||
|
||||
ContactInfo& from = contacts[i];
|
||||
|
||||
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
|
||||
return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len);
|
||||
}
|
||||
|
||||
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
|
||||
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
|
||||
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||
memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect()
|
||||
from.lastmod = getRTCClock()->getCurrentTime();
|
||||
|
||||
onContactPathUpdated(from);
|
||||
|
||||
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
||||
// also got an encoded ACK!
|
||||
if (processAck(extra)) {
|
||||
if (processAck(extra) != NULL) {
|
||||
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||
}
|
||||
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
|
||||
@@ -251,12 +264,25 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
|
||||
}
|
||||
|
||||
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||
if (processAck((uint8_t *)&ack_crc)) {
|
||||
ContactInfo* from;
|
||||
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
|
||||
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
||||
|
||||
if (packet->isRouteFlood() && from->out_path_len >= 0) {
|
||||
// we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
|
||||
handleReturnPathRetry(*from, packet->path, packet->path_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) {
|
||||
// NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY)
|
||||
// override this method in various firmwares, if there's a better strategy
|
||||
mesh::Packet* rpath = createPathReturn(contact.id, contact.shared_secret, path, path_len, 0, NULL, 0);
|
||||
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
|
||||
}
|
||||
|
||||
#ifdef MAX_GROUP_CHANNELS
|
||||
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
|
||||
int n = 0;
|
||||
@@ -543,7 +569,7 @@ void BaseChatMesh::markConnectionActive(const ContactInfo& contact) {
|
||||
}
|
||||
}
|
||||
|
||||
bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
|
||||
ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
|
||||
for (int i = 0; i < MAX_CONNECTIONS; i++) {
|
||||
if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) {
|
||||
// yes, got an ack for our keep_alive request!
|
||||
@@ -552,10 +578,12 @@ bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
|
||||
|
||||
// re-schedule next KEEP_ALIVE, now that we have heard from server
|
||||
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
|
||||
return true; // yes, a match
|
||||
|
||||
auto id = &connections[i].server_id;
|
||||
return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match
|
||||
}
|
||||
}
|
||||
return false; /// no match
|
||||
return NULL; /// no match
|
||||
}
|
||||
|
||||
void BaseChatMesh::checkConnections() {
|
||||
|
||||
@@ -72,6 +72,7 @@ class BaseChatMesh : public mesh::Mesh {
|
||||
ConnectionInfo connections[MAX_CONNECTIONS];
|
||||
|
||||
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
|
||||
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
|
||||
|
||||
protected:
|
||||
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
|
||||
@@ -87,11 +88,14 @@ protected:
|
||||
memset(connections, 0, sizeof(connections));
|
||||
}
|
||||
|
||||
void resetContacts() { num_contacts = 0; }
|
||||
|
||||
// 'UI' concepts, for sub-classes to implement
|
||||
virtual bool isAutoAddEnabled() const { return true; }
|
||||
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
|
||||
virtual bool processAck(const uint8_t *data) = 0;
|
||||
virtual ContactInfo* processAck(const uint8_t *data) = 0;
|
||||
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
|
||||
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
|
||||
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
|
||||
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
|
||||
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
|
||||
@@ -101,6 +105,7 @@ protected:
|
||||
virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0;
|
||||
virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0;
|
||||
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
|
||||
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
|
||||
|
||||
// storage concepts, for sub-classes to override/implement
|
||||
virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented
|
||||
@@ -123,7 +128,7 @@ protected:
|
||||
void stopConnection(const uint8_t* pub_key);
|
||||
bool hasConnectionTo(const uint8_t* pub_key);
|
||||
void markConnectionActive(const ContactInfo& contact);
|
||||
bool checkConnectionsAck(const uint8_t* data);
|
||||
ContactInfo* checkConnectionsAck(const uint8_t* data);
|
||||
void checkConnections();
|
||||
|
||||
public:
|
||||
|
||||
130
src/helpers/ClientACL.cpp
Normal file
130
src/helpers/ClientACL.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "ClientACL.h"
|
||||
|
||||
static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
_fs->remove(filename);
|
||||
return _fs->open(filename, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return _fs->open(filename, "w");
|
||||
#else
|
||||
return _fs->open(filename, "w", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientACL::load(FILESYSTEM* _fs) {
|
||||
num_clients = 0;
|
||||
if (_fs->exists("/s_contacts")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = _fs->open("/s_contacts", "r");
|
||||
#else
|
||||
File file = _fs->open("/s_contacts");
|
||||
#endif
|
||||
if (file) {
|
||||
bool full = false;
|
||||
while (!full) {
|
||||
ClientInfo c;
|
||||
uint8_t pub_key[32];
|
||||
uint8_t unused[2];
|
||||
|
||||
memset(&c, 0, sizeof(c));
|
||||
|
||||
bool success = (file.read(pub_key, 32) == 32);
|
||||
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
|
||||
success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4);
|
||||
success = success && (file.read(unused, 2) == 2);
|
||||
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
||||
success = success && (file.read(c.out_path, 64) == 64);
|
||||
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
|
||||
if (!success) break; // EOF
|
||||
|
||||
c.id = mesh::Identity(pub_key);
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
clients[num_clients++] = c;
|
||||
} else {
|
||||
full = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) {
|
||||
File file = openWrite(_fs, "/s_contacts");
|
||||
if (file) {
|
||||
uint8_t unused[2];
|
||||
memset(unused, 0, sizeof(unused));
|
||||
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
auto c = &clients[i];
|
||||
if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function
|
||||
|
||||
bool success = (file.write(c->id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
|
||||
success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4);
|
||||
success = success && (file.write(unused, 2) == 2);
|
||||
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
|
||||
success = success && (file.write(c->out_path, 64) == 64);
|
||||
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
|
||||
if (!success) break; // write failed
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
|
||||
}
|
||||
return NULL; // not found
|
||||
}
|
||||
|
||||
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
uint32_t min_time = 0xFFFFFFFF;
|
||||
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
if (id.matches(clients[i].id)) return &clients[i]; // already known
|
||||
if (!clients[i].isAdmin() && clients[i].last_activity < min_time) {
|
||||
oldest = &clients[i];
|
||||
min_time = oldest->last_activity;
|
||||
}
|
||||
}
|
||||
|
||||
ClientInfo* c;
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
c = &clients[num_clients++];
|
||||
} else {
|
||||
c = oldest; // evict least active contact
|
||||
}
|
||||
memset(c, 0, sizeof(*c));
|
||||
c->permissions = init_perms;
|
||||
c->id = id;
|
||||
c->out_path_len = -1; // initially out_path is unknown
|
||||
return c;
|
||||
}
|
||||
|
||||
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
|
||||
ClientInfo* c;
|
||||
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
|
||||
c = getClient(pubkey, key_len);
|
||||
if (c == NULL) return false; // partial pubkey not found
|
||||
|
||||
num_clients--; // delete from contacts[]
|
||||
int i = c - clients;
|
||||
while (i < num_clients) {
|
||||
clients[i] = clients[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
|
||||
|
||||
mesh::Identity id(pubkey);
|
||||
c = putClient(id, 0);
|
||||
|
||||
c->permissions = perms; // update their permissions
|
||||
self_id.calcSharedSecret(c->shared_secret, pubkey);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
56
src/helpers/ClientACL.h
Normal file
56
src/helpers/ClientACL.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
|
||||
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
|
||||
#define PERM_ACL_GUEST 0
|
||||
#define PERM_ACL_READ_ONLY 1
|
||||
#define PERM_ACL_READ_WRITE 2
|
||||
#define PERM_ACL_ADMIN 3
|
||||
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint8_t permissions;
|
||||
int8_t out_path_len;
|
||||
uint8_t out_path[MAX_PATH_SIZE];
|
||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
uint32_t last_timestamp; // by THEIR clock (transient)
|
||||
uint32_t last_activity; // by OUR clock (transient)
|
||||
union {
|
||||
struct {
|
||||
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
|
||||
uint32_t pending_ack;
|
||||
uint32_t push_post_timestamp;
|
||||
unsigned long ack_timeout;
|
||||
uint8_t push_failures;
|
||||
} room;
|
||||
} extra;
|
||||
|
||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 20
|
||||
#endif
|
||||
|
||||
class ClientACL {
|
||||
ClientInfo clients[MAX_CLIENTS];
|
||||
int num_clients;
|
||||
|
||||
public:
|
||||
ClientACL() {
|
||||
memset(clients, 0, sizeof(clients));
|
||||
num_clients = 0;
|
||||
}
|
||||
void load(FILESYSTEM* _fs);
|
||||
void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
|
||||
|
||||
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
|
||||
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);
|
||||
bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms);
|
||||
|
||||
int getNumClients() const { return num_clients; }
|
||||
ClientInfo* getClientByIdx(int idx) { return &clients[idx]; }
|
||||
};
|
||||
@@ -51,7 +51,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
|
||||
file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
|
||||
file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
||||
file.read((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115
|
||||
file.read((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
||||
file.read((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116
|
||||
file.read((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
|
||||
file.read(pad, 3); // 121
|
||||
@@ -69,6 +69,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->sf = constrain(_prefs->sf, 7, 12);
|
||||
_prefs->cr = constrain(_prefs->cr, 5, 8);
|
||||
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
|
||||
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -106,7 +107,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112
|
||||
file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113
|
||||
file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
||||
file.write((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115
|
||||
file.write((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
||||
file.write((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116
|
||||
file.write((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
|
||||
file.write(pad, 3); // 121
|
||||
@@ -131,7 +132,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
if (memcmp(command, "reboot", 6) == 0) {
|
||||
_board->reboot(); // doesn't return
|
||||
} else if (memcmp(command, "advert", 6) == 0) {
|
||||
_callbacks->sendSelfAdvertisement(400);
|
||||
_callbacks->sendSelfAdvertisement(1500); // longer delay, give CLI response time to be sent first
|
||||
strcpy(reply, "OK - Advert sent");
|
||||
} else if (memcmp(command, "clock sync", 10) == 0) {
|
||||
uint32_t curr = getRTCClock()->getCurrentTime();
|
||||
@@ -164,6 +165,32 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
}
|
||||
} else if (memcmp(command, "neighbors", 9) == 0) {
|
||||
_callbacks->formatNeighborsReply(reply);
|
||||
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
|
||||
const char* hex = &command[16];
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
|
||||
int pubkey_len = hex_len / 2;
|
||||
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
|
||||
_callbacks->removeNeighbor(pubkey, pubkey_len);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "ERR: bad pubkey");
|
||||
}
|
||||
} else if (memcmp(command, "tempradio ", 10) == 0) {
|
||||
strcpy(tmp, &command[10]);
|
||||
const char *parts[5];
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, 5);
|
||||
float freq = num > 0 ? atof(parts[0]) : 0.0f;
|
||||
float bw = num > 1 ? atof(parts[1]) : 0.0f;
|
||||
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
||||
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
||||
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
|
||||
if (freq >= 300.0f && freq <= 2500.0f && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
|
||||
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
|
||||
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid params");
|
||||
}
|
||||
} else if (memcmp(command, "password ", 9) == 0) {
|
||||
// change admin password
|
||||
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
|
||||
@@ -180,6 +207,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
|
||||
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
||||
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
|
||||
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
||||
@@ -188,6 +217,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
|
||||
} else if (memcmp(config, "guest.password", 14) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->guest_password);
|
||||
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
|
||||
uint8_t prv_key[PRV_KEY_SIZE];
|
||||
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
|
||||
mesh::Utils::toHex(tmp, prv_key, len);
|
||||
sprintf(reply, "> %s", tmp);
|
||||
} else if (memcmp(config, "name", 4) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->node_name);
|
||||
} else if (memcmp(config, "repeat", 6) == 0) {
|
||||
@@ -215,7 +249,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
|
||||
} else if (memcmp(config, "public.key", 10) == 0) {
|
||||
strcpy(reply, "> ");
|
||||
mesh::Utils::toHex(&reply[2], _callbacks->getSelfIdPubKey(), PUB_KEY_SIZE);
|
||||
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
||||
} else if (memcmp(config, "role", 4) == 0) {
|
||||
sprintf(reply, "> %s", _callbacks->getRole());
|
||||
} else {
|
||||
@@ -234,6 +268,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
|
||||
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
|
||||
savePrefs();
|
||||
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
||||
} else if (memcmp(config, "multi.acks ", 11) == 0) {
|
||||
_prefs->multi_acks = atoi(&config[11]);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
|
||||
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
|
||||
@@ -263,6 +301,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (sender_timestamp == 0 && memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
|
||||
uint8_t prv_key[PRV_KEY_SIZE];
|
||||
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
|
||||
if (success) {
|
||||
mesh::LocalIdentity new_id;
|
||||
new_id.readFrom(prv_key, PRV_KEY_SIZE);
|
||||
_callbacks->saveIdentity(new_id);
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid key");
|
||||
}
|
||||
} else if (memcmp(config, "name ", 5) == 0) {
|
||||
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
|
||||
savePrefs();
|
||||
|
||||
@@ -21,7 +21,7 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t allow_read_only;
|
||||
uint8_t reserved2;
|
||||
uint8_t multi_acks;
|
||||
float bw;
|
||||
uint8_t flood_max;
|
||||
uint8_t interference_threshold;
|
||||
@@ -43,8 +43,13 @@ public:
|
||||
virtual void dumpLogFile() = 0;
|
||||
virtual void setTxPower(uint8_t power_dbm) = 0;
|
||||
virtual void formatNeighborsReply(char *reply) = 0;
|
||||
virtual const uint8_t* getSelfIdPubKey() = 0;
|
||||
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
|
||||
// no op by default
|
||||
};
|
||||
virtual mesh::LocalIdentity& getSelfId() = 0;
|
||||
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
|
||||
virtual void clearStats() = 0;
|
||||
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
|
||||
};
|
||||
|
||||
class CommonCLI {
|
||||
@@ -52,7 +57,7 @@ class CommonCLI {
|
||||
NodePrefs* _prefs;
|
||||
CommonCLICallbacks* _callbacks;
|
||||
mesh::MainBoard* _board;
|
||||
char tmp[80];
|
||||
char tmp[PRV_KEY_SIZE*2 + 4];
|
||||
|
||||
mesh::RTCClock* getRTCClock() { return _rtc; }
|
||||
void savePrefs();
|
||||
|
||||
@@ -85,8 +85,7 @@ public:
|
||||
}
|
||||
|
||||
void powerOff() override {
|
||||
// TODO: re-enable this when there is a definite wake-up source pin:
|
||||
// enterDeepSleep(0);
|
||||
enterDeepSleep(0);
|
||||
}
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
#include <Identity.h>
|
||||
|
||||
class IdentityStore {
|
||||
@@ -18,7 +17,8 @@ class IdentityStore {
|
||||
public:
|
||||
IdentityStore(FILESYSTEM& fs, const char* dir): _fs(&fs), _dir(dir) { }
|
||||
|
||||
void begin() { if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
|
||||
void begin() {
|
||||
if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
|
||||
bool load(const char *name, mesh::LocalIdentity& id);
|
||||
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
|
||||
bool save(const char *name, const mesh::LocalIdentity& id);
|
||||
|
||||
@@ -5,25 +5,25 @@
|
||||
class RefCountedDigitalPin {
|
||||
uint8_t _pin;
|
||||
int8_t _claims = 0;
|
||||
|
||||
uint8_t _active = 0;
|
||||
public:
|
||||
RefCountedDigitalPin(uint8_t pin): _pin(pin) { }
|
||||
RefCountedDigitalPin(uint8_t pin,uint8_t active=HIGH): _pin(pin), _active(active) { }
|
||||
|
||||
void begin() {
|
||||
pinMode(_pin, OUTPUT);
|
||||
digitalWrite(_pin, LOW); // initial state
|
||||
digitalWrite(_pin, !_active); // initial state
|
||||
}
|
||||
|
||||
void claim() {
|
||||
_claims++;
|
||||
if (_claims > 0) {
|
||||
digitalWrite(_pin, HIGH);
|
||||
digitalWrite(_pin, _active);
|
||||
}
|
||||
}
|
||||
void release() {
|
||||
_claims--;
|
||||
if (_claims == 0) {
|
||||
digitalWrite(_pin, LOW);
|
||||
digitalWrite(_pin, !_active);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
36
src/helpers/bridges/BridgeBase.cpp
Normal file
36
src/helpers/bridges/BridgeBase.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "BridgeBase.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char *BridgeBase::getLogDateTime() {
|
||||
static char tmp[32];
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
|
||||
dt.year());
|
||||
return tmp;
|
||||
}
|
||||
|
||||
uint16_t BridgeBase::fletcher16(const uint8_t *data, size_t len) {
|
||||
uint8_t sum1 = 0, sum2 = 0;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum1 = (sum1 + data[i]) % 255;
|
||||
sum2 = (sum2 + sum1) % 255;
|
||||
}
|
||||
|
||||
return (sum2 << 8) | sum1;
|
||||
}
|
||||
|
||||
bool BridgeBase::validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum) {
|
||||
uint16_t calculated_checksum = fletcher16(data, len);
|
||||
return received_checksum == calculated_checksum;
|
||||
}
|
||||
|
||||
void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
|
||||
if (!_seen_packets.hasSeen(packet)) {
|
||||
_mgr->queueInbound(packet, millis() + BRIDGE_DELAY);
|
||||
} else {
|
||||
_mgr->free(packet);
|
||||
}
|
||||
}
|
||||
112
src/helpers/bridges/BridgeBase.h
Normal file
112
src/helpers/bridges/BridgeBase.h
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include "helpers/AbstractBridge.h"
|
||||
#include "helpers/SimpleMeshTables.h"
|
||||
|
||||
#include <RTClib.h>
|
||||
|
||||
/**
|
||||
* @brief Base class implementing common bridge functionality
|
||||
*
|
||||
* This class provides common functionality used by different bridge implementations
|
||||
* like packet tracking, checksum calculation, timestamping, and duplicate detection.
|
||||
*
|
||||
* Features:
|
||||
* - Fletcher-16 checksum calculation for data integrity
|
||||
* - Packet duplicate detection using SimpleMeshTables
|
||||
* - Common timestamp formatting for debug logging
|
||||
* - Shared packet management and queuing logic
|
||||
*/
|
||||
class BridgeBase : public AbstractBridge {
|
||||
public:
|
||||
virtual ~BridgeBase() = default;
|
||||
|
||||
/**
|
||||
* @brief Common magic number used by all bridge implementations for packet identification
|
||||
*
|
||||
* This magic number is placed at the beginning of bridge packets to identify
|
||||
* them as mesh bridge packets and provide frame synchronization.
|
||||
*/
|
||||
static constexpr uint16_t BRIDGE_PACKET_MAGIC = 0xC03E;
|
||||
|
||||
/**
|
||||
* @brief Common field sizes used by bridge implementations
|
||||
*
|
||||
* These constants define the size of common packet fields used across bridges.
|
||||
* BRIDGE_MAGIC_SIZE is used by all bridges for packet identification.
|
||||
* BRIDGE_LENGTH_SIZE is used by bridges that need explicit length fields (like RS232).
|
||||
* BRIDGE_CHECKSUM_SIZE is used by all bridges for Fletcher-16 checksums.
|
||||
*/
|
||||
static constexpr uint16_t BRIDGE_MAGIC_SIZE = sizeof(BRIDGE_PACKET_MAGIC);
|
||||
static constexpr uint16_t BRIDGE_LENGTH_SIZE = sizeof(uint16_t);
|
||||
static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
|
||||
|
||||
/**
|
||||
* @brief Default delay in milliseconds for scheduling inbound packet processing
|
||||
*
|
||||
* It provides a buffer to prevent immediate processing conflicts in the mesh network.
|
||||
* Used in handleReceivedPacket() as: millis() + BRIDGE_DELAY
|
||||
*/
|
||||
static constexpr uint16_t BRIDGE_DELAY = 500; // TODO: maybe too high ?
|
||||
|
||||
protected:
|
||||
/** Packet manager for allocating and queuing mesh packets */
|
||||
mesh::PacketManager *_mgr;
|
||||
|
||||
/** RTC clock for timestamping debug messages */
|
||||
mesh::RTCClock *_rtc;
|
||||
|
||||
/** Tracks seen packets to prevent loops in broadcast communications */
|
||||
SimpleMeshTables _seen_packets;
|
||||
|
||||
/**
|
||||
* @brief Constructs a BridgeBase instance
|
||||
*
|
||||
* @param mgr PacketManager for allocating and queuing packets
|
||||
* @param rtc RTCClock for timestamping debug messages
|
||||
*/
|
||||
BridgeBase(mesh::PacketManager *mgr, mesh::RTCClock *rtc) : _mgr(mgr), _rtc(rtc) {}
|
||||
|
||||
/**
|
||||
* @brief Gets formatted date/time string for logging
|
||||
*
|
||||
* Format: "HH:MM:SS - DD/MM/YYYY U"
|
||||
*
|
||||
* @return Formatted date/time string
|
||||
*/
|
||||
const char *getLogDateTime();
|
||||
|
||||
/**
|
||||
* @brief Calculate Fletcher-16 checksum
|
||||
*
|
||||
* Based on: https://en.wikipedia.org/wiki/Fletcher%27s_checksum
|
||||
* Used to verify data integrity of received packets
|
||||
*
|
||||
* @param data Pointer to data to calculate checksum for
|
||||
* @param len Length of data in bytes
|
||||
* @return Calculated Fletcher-16 checksum
|
||||
*/
|
||||
static uint16_t fletcher16(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Validate received checksum against calculated checksum
|
||||
*
|
||||
* @param data Pointer to data to validate
|
||||
* @param len Length of data in bytes
|
||||
* @param received_checksum Checksum received with data
|
||||
* @return true if checksum is valid, false otherwise
|
||||
*/
|
||||
bool validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum);
|
||||
|
||||
/**
|
||||
* @brief Common packet handling for received packets
|
||||
*
|
||||
* Implements the standard pattern used by all bridges:
|
||||
* - Check if packet was seen before using _seen_packets.hasSeen()
|
||||
* - Queue packet for mesh processing if not seen before
|
||||
* - Free packet if already seen to prevent duplicates
|
||||
*
|
||||
* @param packet The received mesh packet
|
||||
*/
|
||||
void handleReceivedPacket(mesh::Packet *packet);
|
||||
};
|
||||
196
src/helpers/bridges/ESPNowBridge.cpp
Normal file
196
src/helpers/bridges/ESPNowBridge.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#include "ESPNowBridge.h"
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
|
||||
// Static member to handle callbacks
|
||||
ESPNowBridge *ESPNowBridge::_instance = nullptr;
|
||||
|
||||
// Static callback wrappers
|
||||
void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len) {
|
||||
if (_instance) {
|
||||
_instance->onDataRecv(mac, data, len);
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
|
||||
if (_instance) {
|
||||
_instance->onDataSent(mac, status);
|
||||
}
|
||||
}
|
||||
|
||||
ESPNowBridge::ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc)
|
||||
: BridgeBase(mgr, rtc), _rx_buffer_pos(0) {
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
void ESPNowBridge::begin() {
|
||||
// Initialize WiFi in station mode
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
// Initialize ESP-NOW
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
Serial.printf("%s: ESPNOW BRIDGE: Error initializing ESP-NOW\n", getLogDateTime());
|
||||
return;
|
||||
}
|
||||
|
||||
// Register callbacks
|
||||
esp_now_register_recv_cb(recv_cb);
|
||||
esp_now_register_send_cb(send_cb);
|
||||
|
||||
// Add broadcast peer
|
||||
esp_now_peer_info_t peerInfo = {};
|
||||
memset(&peerInfo, 0, sizeof(peerInfo));
|
||||
memset(peerInfo.peer_addr, 0xFF, ESP_NOW_ETH_ALEN); // Broadcast address
|
||||
peerInfo.channel = 0;
|
||||
peerInfo.encrypt = false;
|
||||
|
||||
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
|
||||
Serial.printf("%s: ESPNOW BRIDGE: Failed to add broadcast peer\n", getLogDateTime());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowBridge::loop() {
|
||||
// Nothing to do here - ESP-NOW is callback based
|
||||
}
|
||||
|
||||
void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
|
||||
size_t keyLen = strlen(_secret);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
data[i] ^= _secret[i % keyLen];
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len) {
|
||||
// Ignore packets that are too small to contain header + checksum
|
||||
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: RX packet too small, len=%d\n", getLogDateTime(), len);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate total packet size
|
||||
if (len > MAX_ESPNOW_PACKET_SIZE) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: RX packet too large, len=%d\n", getLogDateTime(), len);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Check packet header magic
|
||||
uint16_t received_magic = (data[0] << 8) | data[1];
|
||||
if (received_magic != BRIDGE_PACKET_MAGIC) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: RX invalid magic 0x%04X\n", getLogDateTime(), received_magic);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a copy we can decrypt
|
||||
uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE];
|
||||
const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE;
|
||||
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
|
||||
|
||||
// Try to decrypt (checksum + payload)
|
||||
xorCrypt(decrypted, encryptedDataLen);
|
||||
|
||||
// Validate checksum
|
||||
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
|
||||
const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE;
|
||||
|
||||
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
|
||||
// Failed to decrypt - likely from a different network
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: RX checksum mismatch, rcv=0x%04X\n", getLogDateTime(),
|
||||
received_checksum);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: RX, payload_len=%d\n", getLogDateTime(), payloadLen);
|
||||
#endif
|
||||
|
||||
// Create mesh packet
|
||||
mesh::Packet *pkt = _instance->_mgr->allocNew();
|
||||
if (!pkt) return;
|
||||
|
||||
if (pkt->readFrom(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen)) {
|
||||
_instance->onPacketReceived(pkt);
|
||||
} else {
|
||||
_instance->_mgr->free(pkt);
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
// Could add transmission error handling here if needed
|
||||
}
|
||||
|
||||
void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
|
||||
handleReceivedPacket(packet);
|
||||
}
|
||||
|
||||
void ESPNowBridge::onPacketTransmitted(mesh::Packet *packet) {
|
||||
// First validate the packet pointer
|
||||
if (!packet) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: TX invalid packet pointer\n", getLogDateTime());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_seen_packets.hasSeen(packet)) {
|
||||
|
||||
// Create a temporary buffer just for size calculation and reuse for actual writing
|
||||
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
|
||||
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
|
||||
|
||||
// Check if packet fits within our maximum payload size
|
||||
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: ESPNOW BRIDGE: TX packet too large (payload=%d, max=%d)\n", getLogDateTime(),
|
||||
meshPacketLen, MAX_PAYLOAD_SIZE);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t buffer[MAX_ESPNOW_PACKET_SIZE];
|
||||
|
||||
// Write magic header (2 bytes)
|
||||
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
|
||||
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;
|
||||
|
||||
// Write packet payload starting after magic header and checksum
|
||||
const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE;
|
||||
memcpy(buffer + packetOffset, sizingBuffer, meshPacketLen);
|
||||
|
||||
// Calculate and add checksum (only of the payload)
|
||||
uint16_t checksum = fletcher16(buffer + packetOffset, meshPacketLen);
|
||||
buffer[2] = (checksum >> 8) & 0xFF; // High byte
|
||||
buffer[3] = checksum & 0xFF; // Low byte
|
||||
|
||||
// Encrypt payload and checksum (not including magic header)
|
||||
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
|
||||
|
||||
// Total packet size: magic header + checksum + payload
|
||||
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
|
||||
|
||||
// Broadcast using ESP-NOW
|
||||
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
esp_err_t result = esp_now_send(broadcastAddress, buffer, totalPacketSize);
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
if (result == ESP_OK) {
|
||||
Serial.printf("%s: ESPNOW BRIDGE: TX, len=%d\n", getLogDateTime(), meshPacketLen);
|
||||
} else {
|
||||
Serial.printf("%s: ESPNOW BRIDGE: TX FAILED!\n", getLogDateTime());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
156
src/helpers/bridges/ESPNowBridge.h
Normal file
156
src/helpers/bridges/ESPNowBridge.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshCore.h"
|
||||
#include "esp_now.h"
|
||||
#include "helpers/bridges/BridgeBase.h"
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
|
||||
#ifndef WITH_ESPNOW_BRIDGE_SECRET
|
||||
#error WITH_ESPNOW_BRIDGE_SECRET must be defined to use ESPNowBridge
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Bridge implementation using ESP-NOW protocol for packet transport
|
||||
*
|
||||
* This bridge enables mesh packet transport over ESP-NOW, a connectionless communication
|
||||
* protocol provided by Espressif that allows ESP32 devices to communicate directly
|
||||
* without WiFi router infrastructure.
|
||||
*
|
||||
* Features:
|
||||
* - Broadcast-based communication (all bridges receive all packets)
|
||||
* - Network isolation using XOR encryption with shared secret
|
||||
* - Duplicate packet detection using SimpleMeshTables tracking
|
||||
* - Maximum packet size of 250 bytes (ESP-NOW limitation)
|
||||
*
|
||||
* Packet Structure:
|
||||
* [2 bytes] Magic Header - Used to identify ESPNowBridge packets
|
||||
* [2 bytes] Fletcher-16 checksum of encrypted payload (calculated over payload only)
|
||||
* [246 bytes max] Encrypted payload containing the mesh packet
|
||||
*
|
||||
* The Fletcher-16 checksum is used to validate packet integrity and detect
|
||||
* corrupted or tampered packets. It's calculated over the encrypted payload
|
||||
* and provides a simple but effective way to verify packets are both
|
||||
* uncorrupted and from the same network (since the checksum is calculated
|
||||
* after encryption).
|
||||
*
|
||||
* Configuration:
|
||||
* - Define WITH_ESPNOW_BRIDGE to enable this bridge
|
||||
* - Define WITH_ESPNOW_BRIDGE_SECRET with a string to set the network encryption key
|
||||
*
|
||||
* Network Isolation:
|
||||
* Multiple independent mesh networks can coexist by using different
|
||||
* WITH_ESPNOW_BRIDGE_SECRET values. Packets encrypted with a different key will
|
||||
* fail the checksum validation and be discarded.
|
||||
*/
|
||||
class ESPNowBridge : public BridgeBase {
|
||||
private:
|
||||
static ESPNowBridge *_instance;
|
||||
static void recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len);
|
||||
static void send_cb(const uint8_t *mac, esp_now_send_status_t status);
|
||||
|
||||
/**
|
||||
* ESP-NOW Protocol Structure:
|
||||
* - ESP-NOW header: 20 bytes (handled by ESP-NOW protocol)
|
||||
* - ESP-NOW payload: 250 bytes maximum
|
||||
* Total ESP-NOW packet: 270 bytes
|
||||
*
|
||||
* Our Bridge Packet Structure (must fit in ESP-NOW payload):
|
||||
* - Magic header: 2 bytes
|
||||
* - Checksum: 2 bytes
|
||||
* - Available payload: 246 bytes
|
||||
*/
|
||||
static const size_t MAX_ESPNOW_PACKET_SIZE = 250;
|
||||
|
||||
/**
|
||||
* Size constants for packet parsing
|
||||
*/
|
||||
static const size_t MAX_PAYLOAD_SIZE = MAX_ESPNOW_PACKET_SIZE - (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE);
|
||||
|
||||
/** Buffer for receiving ESP-NOW packets */
|
||||
uint8_t _rx_buffer[MAX_ESPNOW_PACKET_SIZE];
|
||||
|
||||
/** Current position in receive buffer */
|
||||
size_t _rx_buffer_pos;
|
||||
|
||||
/**
|
||||
* Network encryption key from build define
|
||||
* Must be defined with WITH_ESPNOW_BRIDGE_SECRET
|
||||
* Used for XOR encryption to isolate different mesh networks
|
||||
*/
|
||||
const char *_secret = WITH_ESPNOW_BRIDGE_SECRET;
|
||||
|
||||
/**
|
||||
* Performs XOR encryption/decryption of data
|
||||
*
|
||||
* Uses WITH_ESPNOW_BRIDGE_SECRET as the key in a simple XOR operation.
|
||||
* The same operation is used for both encryption and decryption.
|
||||
* While not cryptographically secure, it provides basic network isolation.
|
||||
*
|
||||
* @param data Pointer to data to encrypt/decrypt
|
||||
* @param len Length of data in bytes
|
||||
*/
|
||||
void xorCrypt(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* ESP-NOW receive callback
|
||||
* Called by ESP-NOW when a packet is received
|
||||
*
|
||||
* @param mac Source MAC address
|
||||
* @param data Received data
|
||||
* @param len Length of received data
|
||||
*/
|
||||
void onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len);
|
||||
|
||||
/**
|
||||
* ESP-NOW send callback
|
||||
* Called by ESP-NOW after a transmission attempt
|
||||
*
|
||||
* @param mac_addr Destination MAC address
|
||||
* @param status Transmission status
|
||||
*/
|
||||
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs an ESPNowBridge instance
|
||||
*
|
||||
* @param mgr PacketManager for allocating and queuing packets
|
||||
* @param rtc RTCClock for timestamping debug messages
|
||||
*/
|
||||
ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
|
||||
/**
|
||||
* Initializes the ESP-NOW bridge
|
||||
*
|
||||
* - Configures WiFi in station mode
|
||||
* - Initializes ESP-NOW protocol
|
||||
* - Registers callbacks
|
||||
* - Sets up broadcast peer
|
||||
*/
|
||||
void begin() override;
|
||||
|
||||
/**
|
||||
* Main loop handler
|
||||
* ESP-NOW is callback-based, so this is currently empty
|
||||
*/
|
||||
void loop() override;
|
||||
|
||||
/**
|
||||
* Called when a packet is received via ESP-NOW
|
||||
* Queues the packet for mesh processing if not seen before
|
||||
*
|
||||
* @param packet The received mesh packet
|
||||
*/
|
||||
void onPacketReceived(mesh::Packet *packet) override;
|
||||
|
||||
/**
|
||||
* Called when a packet needs to be transmitted via ESP-NOW
|
||||
* Encrypts and broadcasts the packet if not seen before
|
||||
*
|
||||
* @param packet The mesh packet to transmit
|
||||
*/
|
||||
void onPacketTransmitted(mesh::Packet *packet) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
147
src/helpers/bridges/RS232Bridge.cpp
Normal file
147
src/helpers/bridges/RS232Bridge.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "RS232Bridge.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
|
||||
RS232Bridge::RS232Bridge(Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
|
||||
: BridgeBase(mgr, rtc), _serial(&serial) {}
|
||||
|
||||
void RS232Bridge::begin() {
|
||||
#if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
|
||||
#error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
|
||||
#endif
|
||||
|
||||
#if defined(ESP32)
|
||||
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
|
||||
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
((HardwareSerial *)_serial)->setRx(WITH_RS232_BRIDGE_RX);
|
||||
((HardwareSerial *)_serial)->setTx(WITH_RS232_BRIDGE_TX);
|
||||
#else
|
||||
#error RS232Bridge was not tested on the current platform
|
||||
#endif
|
||||
((HardwareSerial *)_serial)->begin(115200);
|
||||
}
|
||||
|
||||
void RS232Bridge::onPacketTransmitted(mesh::Packet *packet) {
|
||||
// First validate the packet pointer
|
||||
if (!packet) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: TX invalid packet pointer\n", getLogDateTime());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_seen_packets.hasSeen(packet)) {
|
||||
|
||||
uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
|
||||
uint16_t len = packet->writeTo(buffer + 4);
|
||||
|
||||
// Check if packet fits within our maximum payload size
|
||||
if (len > (MAX_TRANS_UNIT + 1)) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: TX packet too large (payload=%d, max=%d)\n", getLogDateTime(), len,
|
||||
MAX_TRANS_UNIT + 1);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Build packet header
|
||||
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
|
||||
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF; // Magic low byte
|
||||
buffer[2] = (len >> 8) & 0xFF; // Length high byte
|
||||
buffer[3] = len & 0xFF; // Length low byte
|
||||
|
||||
// Calculate checksum over the payload
|
||||
uint16_t checksum = fletcher16(buffer + 4, len);
|
||||
buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
|
||||
buffer[5 + len] = checksum & 0xFF; // Checksum low byte
|
||||
|
||||
// Send complete packet
|
||||
_serial->write(buffer, len + SERIAL_OVERHEAD);
|
||||
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: TX, len=%d crc=0x%04x\n", getLogDateTime(), len, checksum);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void RS232Bridge::loop() {
|
||||
while (_serial->available()) {
|
||||
uint8_t b = _serial->read();
|
||||
|
||||
if (_rx_buffer_pos < 2) {
|
||||
// Waiting for magic word
|
||||
if ((_rx_buffer_pos == 0 && b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) ||
|
||||
(_rx_buffer_pos == 1 && b == (BRIDGE_PACKET_MAGIC & 0xFF))) {
|
||||
_rx_buffer[_rx_buffer_pos++] = b;
|
||||
} else {
|
||||
// Invalid magic byte, reset and start over
|
||||
_rx_buffer_pos = 0;
|
||||
// Check if this byte could be the start of a new magic word
|
||||
if (b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) {
|
||||
_rx_buffer[_rx_buffer_pos++] = b;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reading length, payload, and checksum
|
||||
_rx_buffer[_rx_buffer_pos++] = b;
|
||||
|
||||
if (_rx_buffer_pos >= 4) {
|
||||
uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
|
||||
|
||||
// Validate length field
|
||||
if (len > (MAX_TRANS_UNIT + 1)) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: RX invalid length %d, resetting\n", getLogDateTime(), len);
|
||||
#endif
|
||||
_rx_buffer_pos = 0; // Invalid length, reset
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_rx_buffer_pos == len + SERIAL_OVERHEAD) { // Full packet received
|
||||
uint16_t received_checksum = (_rx_buffer[4 + len] << 8) | _rx_buffer[5 + len];
|
||||
|
||||
if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: RX, len=%d crc=0x%04x\n", getLogDateTime(), len,
|
||||
received_checksum);
|
||||
#endif
|
||||
mesh::Packet *pkt = _mgr->allocNew();
|
||||
if (pkt) {
|
||||
if (pkt->readFrom(_rx_buffer + 4, len)) {
|
||||
onPacketReceived(pkt);
|
||||
} else {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: RX failed to parse packet\n", getLogDateTime());
|
||||
#endif
|
||||
_mgr->free(pkt);
|
||||
}
|
||||
} else {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: RX failed to allocate packet\n", getLogDateTime());
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#if MESH_PACKET_LOGGING
|
||||
Serial.printf("%s: RS232 BRIDGE: RX checksum mismatch, rcv=0x%04x\n", getLogDateTime(),
|
||||
received_checksum);
|
||||
#endif
|
||||
}
|
||||
_rx_buffer_pos = 0; // Reset for next packet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RS232Bridge::onPacketReceived(mesh::Packet *packet) {
|
||||
handleReceivedPacket(packet);
|
||||
}
|
||||
|
||||
#endif
|
||||
141
src/helpers/bridges/RS232Bridge.h
Normal file
141
src/helpers/bridges/RS232Bridge.h
Normal file
@@ -0,0 +1,141 @@
|
||||
#pragma once
|
||||
|
||||
#include "helpers/bridges/BridgeBase.h"
|
||||
|
||||
#include <Stream.h>
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
|
||||
/**
|
||||
* @brief Bridge implementation using RS232/UART protocol for packet transport
|
||||
*
|
||||
* This bridge enables mesh packet transport over serial/UART connections,
|
||||
* allowing nodes to communicate over wired serial links. It implements a simple
|
||||
* packet framing protocol with checksums for reliable transfer.
|
||||
*
|
||||
* Features:
|
||||
* - Point-to-point communication over hardware UART
|
||||
* - Fletcher-16 checksum for data integrity verification
|
||||
* - Magic header for packet synchronization and frame alignment
|
||||
* - Duplicate packet detection using SimpleMeshTables tracking
|
||||
* - Configurable RX/TX pins via build defines
|
||||
* - Fixed baud rate at 115200 for consistent timing
|
||||
*
|
||||
* Packet Structure:
|
||||
* [2 bytes] Magic Header (0xC03E) - Used to identify start of RS232Bridge packets
|
||||
* [2 bytes] Payload Length - Length of the mesh packet payload
|
||||
* [n bytes] Mesh Packet Payload - The actual mesh packet data
|
||||
* [2 bytes] Fletcher-16 Checksum - Calculated over the payload for integrity verification
|
||||
*
|
||||
* The Fletcher-16 checksum is calculated over the mesh packet payload and provides
|
||||
* error detection capabilities suitable for serial communication where electrical
|
||||
* noise, timing issues, or hardware problems could corrupt data. The checksum
|
||||
* validation ensures only valid packets are forwarded to the mesh.
|
||||
*
|
||||
* Configuration:
|
||||
* - Define WITH_RS232_BRIDGE to enable this bridge
|
||||
* - Define WITH_RS232_BRIDGE_RX with the RX pin number
|
||||
* - Define WITH_RS232_BRIDGE_TX with the TX pin number
|
||||
*
|
||||
* Platform Support:
|
||||
* Different platforms require different pin configuration methods:
|
||||
* - ESP32: Uses HardwareSerial::setPins(rx, tx)
|
||||
* - NRF52: Uses HardwareSerial::setPins(rx, tx)
|
||||
* - RP2040: Uses SerialUART::setRX(rx) and SerialUART::setTX(tx)
|
||||
* - STM32: Uses HardwareSerial::setRx(rx) and HardwareSerial::setTx(tx)
|
||||
*/
|
||||
class RS232Bridge : public BridgeBase {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs an RS232Bridge instance
|
||||
*
|
||||
* @param serial The hardware serial port to use
|
||||
* @param mgr PacketManager for allocating and queuing packets
|
||||
* @param rtc RTCClock for timestamping debug messages
|
||||
*/
|
||||
RS232Bridge(Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
|
||||
/**
|
||||
* Initializes the RS232 bridge
|
||||
*
|
||||
* - Validates that RX/TX pins are defined
|
||||
* - Configures UART pins based on target platform
|
||||
* - Sets baud rate to 115200 for consistent communication
|
||||
* - Platform-specific pin configuration methods are used
|
||||
*/
|
||||
void begin() override;
|
||||
|
||||
/**
|
||||
* @brief Main loop handler for processing incoming serial data
|
||||
*
|
||||
* Implements a state machine for packet reception:
|
||||
* 1. Searches for magic header bytes for packet synchronization
|
||||
* 2. Reads length field to determine expected packet size
|
||||
* 3. Validates packet length against maximum allowed size
|
||||
* 4. Receives complete packet payload and checksum
|
||||
* 5. Validates Fletcher-16 checksum for data integrity
|
||||
* 6. Creates mesh packet and forwards if valid
|
||||
*/
|
||||
void loop() override;
|
||||
|
||||
/**
|
||||
* @brief Called when a packet needs to be transmitted over serial
|
||||
*
|
||||
* Formats the mesh packet with RS232 framing protocol:
|
||||
* - Adds magic header for synchronization
|
||||
* - Includes payload length field
|
||||
* - Calculates Fletcher-16 checksum over payload
|
||||
* - Transmits complete framed packet
|
||||
* - Uses duplicate detection to prevent retransmission
|
||||
*
|
||||
* @param packet The mesh packet to transmit
|
||||
*/
|
||||
void onPacketTransmitted(mesh::Packet *packet) override;
|
||||
|
||||
/**
|
||||
* @brief Called when a complete valid packet has been received from serial
|
||||
*
|
||||
* Forwards the received packet to the mesh for processing.
|
||||
* The packet has already been validated for checksum integrity
|
||||
* and parsed successfully at this point.
|
||||
*
|
||||
* @param packet The received mesh packet ready for processing
|
||||
*/
|
||||
void onPacketReceived(mesh::Packet *packet) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* RS232 Protocol Structure:
|
||||
* - Magic header: 2 bytes (packet identification)
|
||||
* - Length field: 2 bytes (payload length)
|
||||
* - Payload: variable bytes (mesh packet data)
|
||||
* - Checksum: 2 bytes (Fletcher-16 over payload)
|
||||
* Total overhead: 6 bytes
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief The total overhead of the serial protocol in bytes.
|
||||
* Includes: MAGIC_WORD (2) + LENGTH (2) + CHECKSUM (2) = 6 bytes
|
||||
*/
|
||||
static constexpr uint16_t SERIAL_OVERHEAD = BRIDGE_MAGIC_SIZE + BRIDGE_LENGTH_SIZE + BRIDGE_CHECKSUM_SIZE;
|
||||
|
||||
/**
|
||||
* @brief The maximum size of a complete packet on the serial line.
|
||||
*
|
||||
* This is calculated as the sum of:
|
||||
* - MAX_TRANS_UNIT + 1 for the maximum mesh packet size
|
||||
* - SERIAL_OVERHEAD for the framing (magic + length + checksum)
|
||||
*/
|
||||
static constexpr uint16_t MAX_SERIAL_PACKET_SIZE = (MAX_TRANS_UNIT + 1) + SERIAL_OVERHEAD;
|
||||
|
||||
/** Hardware serial port interface */
|
||||
Stream *_serial;
|
||||
|
||||
/** Buffer for building received packets */
|
||||
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
|
||||
|
||||
/** Current position in the receive buffer */
|
||||
uint16_t _rx_buffer_pos = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -66,7 +66,7 @@ bool SerialBLEInterface::onSecurityRequest() {
|
||||
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
|
||||
if (cmpl.success) {
|
||||
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
|
||||
//deviceConnected = true;
|
||||
deviceConnected = true;
|
||||
} else {
|
||||
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
|
||||
|
||||
@@ -83,12 +83,11 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
|
||||
|
||||
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
|
||||
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
|
||||
last_conn_id = param->connect.conn_id;
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
|
||||
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
|
||||
|
||||
deviceConnected = true;
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
|
||||
@@ -143,6 +142,7 @@ void SerialBLEInterface::disable() {
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
|
||||
|
||||
pServer->getAdvertising()->stop();
|
||||
pServer->disconnect(last_conn_id);
|
||||
pService->stop();
|
||||
oldDeviceConnected = deviceConnected = false;
|
||||
adv_restart_time = 0;
|
||||
|
||||
@@ -13,6 +13,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
|
||||
bool deviceConnected;
|
||||
bool oldDeviceConnected;
|
||||
bool _isEnabled;
|
||||
uint16_t last_conn_id;
|
||||
uint32_t _pin_code;
|
||||
unsigned long _last_write;
|
||||
unsigned long adv_restart_time;
|
||||
@@ -56,6 +57,7 @@ public:
|
||||
adv_restart_time = 0;
|
||||
_isEnabled = false;
|
||||
_last_write = 0;
|
||||
last_conn_id = 0;
|
||||
send_queue_len = recv_queue_len = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,18 @@ bool SerialWifiInterface::isWriteBusy() const {
|
||||
}
|
||||
|
||||
size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
if (!client) client = server.available();
|
||||
// check if new client connected
|
||||
auto newClient = server.available();
|
||||
if (newClient) {
|
||||
|
||||
// disconnect existing client
|
||||
deviceConnected = false;
|
||||
client.stop();
|
||||
|
||||
// switch active connection to new client
|
||||
client = newClient;
|
||||
|
||||
}
|
||||
|
||||
if (client.connected()) {
|
||||
if (!deviceConnected) {
|
||||
|
||||
@@ -4,10 +4,7 @@ static SerialBLEInterface* instance;
|
||||
|
||||
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected");
|
||||
if(instance){
|
||||
instance->_isDeviceConnected = true;
|
||||
// no need to stop advertising on connect, as the ble stack does this automatically
|
||||
}
|
||||
// we now set _isDeviceConnected=true in onSecured callback instead
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
|
||||
@@ -18,6 +15,14 @@ void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason
|
||||
}
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured");
|
||||
if(instance){
|
||||
instance->_isDeviceConnected = true;
|
||||
// no need to stop advertising on connect, as the ble stack does this automatically
|
||||
}
|
||||
}
|
||||
|
||||
void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
|
||||
|
||||
instance = this;
|
||||
@@ -27,8 +32,8 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
|
||||
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.configPrphConn(250, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); // increase MTU
|
||||
Bluefruit.setTxPower(BLE_TX_POWER);
|
||||
Bluefruit.begin();
|
||||
Bluefruit.setTxPower(4); // Check bluefruit.h for supported values
|
||||
Bluefruit.setName(device_name);
|
||||
|
||||
Bluefruit.Security.setMITM(true);
|
||||
@@ -36,6 +41,7 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
|
||||
|
||||
Bluefruit.Periph.setConnectCallback(onConnect);
|
||||
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
|
||||
Bluefruit.Security.setSecuredCallback(onSecured);
|
||||
|
||||
// To be consistent OTA DFU should be added first if it exists
|
||||
//bledfu.begin();
|
||||
@@ -80,7 +86,7 @@ void SerialBLEInterface::startAdv() {
|
||||
* https://developer.apple.com/library/content/qa/qa1931/_index.html
|
||||
*/
|
||||
Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
|
||||
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
|
||||
Bluefruit.Advertising.setInterval(32, 244);
|
||||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
||||
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
|
||||
|
||||
@@ -115,6 +121,20 @@ void SerialBLEInterface::enable() {
|
||||
void SerialBLEInterface::disable() {
|
||||
_isEnabled = false;
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
|
||||
|
||||
#ifdef RAK_BOARD
|
||||
Bluefruit.disconnect(Bluefruit.connHandle());
|
||||
#else
|
||||
uint16_t conn_id;
|
||||
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) {
|
||||
Bluefruit.disconnect(conn_id);
|
||||
}
|
||||
#endif
|
||||
|
||||
Bluefruit.Advertising.restartOnDisconnect(false);
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.Advertising.clearData();
|
||||
|
||||
stopAdv();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "../BaseSerialInterface.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
#ifndef BLE_TX_POWER
|
||||
#define BLE_TX_POWER 4
|
||||
#endif
|
||||
|
||||
class SerialBLEInterface : public BaseSerialInterface {
|
||||
BLEUart bleuart;
|
||||
bool _isEnabled;
|
||||
@@ -21,6 +25,7 @@ class SerialBLEInterface : public BaseSerialInterface {
|
||||
void clearBuffers() { send_queue_len = 0; }
|
||||
static void onConnect(uint16_t connection_handle);
|
||||
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
|
||||
static void onSecured(uint16_t connection_handle);
|
||||
|
||||
public:
|
||||
SerialBLEInterface() {
|
||||
|
||||
@@ -96,8 +96,9 @@ bool RadioLibWrapper::isInRecvMode() const {
|
||||
}
|
||||
|
||||
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
|
||||
int len = 0;
|
||||
if (state & STATE_INT_READY) {
|
||||
int len = _radio->getPacketLength();
|
||||
len = _radio->getPacketLength();
|
||||
if (len > 0) {
|
||||
if (len > sz) { len = sz; }
|
||||
int err = _radio->readData(bytes, len);
|
||||
@@ -110,7 +111,6 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
|
||||
}
|
||||
}
|
||||
state = STATE_IDLE; // need another startReceive()
|
||||
return len;
|
||||
}
|
||||
|
||||
if (state != STATE_RX) {
|
||||
@@ -121,7 +121,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return len;
|
||||
}
|
||||
|
||||
uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
|
||||
@@ -1,5 +1,11 @@
|
||||
#include "EnvironmentSensorManager.h"
|
||||
|
||||
#if ENV_PIN_SDA && ENV_PIN_SCL
|
||||
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
|
||||
#else
|
||||
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address
|
||||
#include <Adafruit_AHTX0.h>
|
||||
@@ -29,6 +35,12 @@ static Adafruit_BMP280 BMP280;
|
||||
static Adafruit_SHTC3 SHTC3;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
|
||||
#include <SensirionI2cSht4x.h>
|
||||
static SensirionI2cSht4x SHT4X;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
#include <Arduino_LPS22HB.h>
|
||||
#endif
|
||||
@@ -47,13 +59,67 @@ static Adafruit_INA3221 INA3221;
|
||||
static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
#define TELEM_INA260_ADDRESS 0x41 // INA260 single channel current sensor I2C address
|
||||
#include <Adafruit_INA260.h>
|
||||
static Adafruit_INA260 INA260;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
#define TELEM_INA226_ADDRESS 0x44
|
||||
#define TELEM_INA226_SHUNT_VALUE 0.100
|
||||
#define TELEM_INA226_MAX_AMP 0.8
|
||||
#include <INA226.h>
|
||||
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
#define TELEM_MLX90614_ADDRESS 0x5A // MLX90614 IR temperature sensor I2C address
|
||||
#include <Adafruit_MLX90614.h>
|
||||
static Adafruit_MLX90614 MLX90614;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
#define TELEM_VL53L0X_ADDRESS 0x29 // VL53L0X time-of-flight distance sensor I2C address
|
||||
#include <Adafruit_VL53L0X.h>
|
||||
static Adafruit_VL53L0X VL53L0X;
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_GPS && defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
#define RAK_WISBLOCK_GPS
|
||||
#endif
|
||||
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
static uint32_t gpsResetPin = 0;
|
||||
static bool i2cGPSFlag = false;
|
||||
static bool serialGPSFlag = false;
|
||||
#define TELEM_RAK12500_ADDRESS 0x42 //RAK12500 Ublox GPS via i2c
|
||||
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
|
||||
static SFE_UBLOX_GNSS ublox_GNSS;
|
||||
#endif
|
||||
|
||||
bool EnvironmentSensorManager::begin() {
|
||||
#if ENV_INCLUDE_GPS
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
rakGPSInit(); //probe base board/sockets for GPS
|
||||
#else
|
||||
initBasicGPS();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if ENV_PIN_SDA && ENV_PIN_SCL
|
||||
#ifdef NRF52_PLATFORM
|
||||
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
|
||||
Wire1.setClock(100000);
|
||||
Wire1.begin();
|
||||
#else
|
||||
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_AHTX0
|
||||
if (AHTX0.begin(&Wire, 0, TELEM_AHTX_ADDRESS)) {
|
||||
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
|
||||
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
|
||||
AHTX0_initialized = true;
|
||||
} else {
|
||||
@@ -63,7 +129,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_BME280
|
||||
if (BME280.begin(TELEM_BME280_ADDRESS, &Wire)) {
|
||||
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
|
||||
BME280_initialized = true;
|
||||
@@ -94,6 +160,21 @@ bool EnvironmentSensorManager::begin() {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
|
||||
uint32_t serialNumber = 0;
|
||||
int16_t sht4x_error;
|
||||
sht4x_error = SHT4X.serialNumber(serialNumber);
|
||||
if (sht4x_error == 0) {
|
||||
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
|
||||
SHT4X_initialized = true;
|
||||
} else {
|
||||
SHT4X_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
if (BARO.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
|
||||
@@ -105,7 +186,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA3221
|
||||
if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) {
|
||||
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
|
||||
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
|
||||
|
||||
@@ -120,7 +201,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA219
|
||||
if (INA219.begin(&Wire)) {
|
||||
if (INA219.begin(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
|
||||
INA219_initialized = true;
|
||||
} else {
|
||||
@@ -129,6 +210,47 @@ bool EnvironmentSensorManager::begin() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
|
||||
INA260_initialized = true;
|
||||
} else {
|
||||
INA260_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
if (INA226.begin()) {
|
||||
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
|
||||
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
|
||||
INA226_initialized = true;
|
||||
} else {
|
||||
INA226_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
|
||||
MLX90614_initialized = true;
|
||||
} else {
|
||||
MLX90614_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
|
||||
VL53L0X_initialized = true;
|
||||
} else {
|
||||
VL53L0X_initialized = false;
|
||||
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,7 +258,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
|
||||
next_available_channel = TELEM_CHANNEL_SELF + 1;
|
||||
|
||||
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
|
||||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, 0.0f); // allow lat/lon via telemetry even if no GPS is detected
|
||||
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
|
||||
}
|
||||
|
||||
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
@@ -162,8 +284,8 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
|
||||
#if ENV_INCLUDE_BMP280
|
||||
if (BMP280_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure());
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
|
||||
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
|
||||
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -177,6 +299,18 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_SHT4X
|
||||
if (SHT4X_initialized) {
|
||||
float sht4x_humidity, sht4x_temperature;
|
||||
int16_t sht4x_error;
|
||||
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
|
||||
if (sht4x_error == 0) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
|
||||
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_LPS22HB
|
||||
if (LPS22HB_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
|
||||
@@ -209,6 +343,43 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA260
|
||||
if (INA260_initialized) {
|
||||
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
|
||||
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
|
||||
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
|
||||
next_available_channel++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_INA226
|
||||
if (INA226_initialized) {
|
||||
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
|
||||
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
|
||||
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
|
||||
next_available_channel++;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_MLX90614
|
||||
if (MLX90614_initialized) {
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_VL53L0X
|
||||
if (VL53L0X_initialized) {
|
||||
VL53L0X_RangingMeasurementData_t measure;
|
||||
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
|
||||
if (measure.RangeStatus != 4) { // phase failures
|
||||
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
|
||||
} else {
|
||||
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -266,10 +437,8 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
#endif
|
||||
|
||||
// Try to detect if GPS is physically connected to determine if we should expose the setting
|
||||
#ifdef PIN_GPS_EN
|
||||
pinMode(PIN_GPS_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_EN, HIGH); // Power on GPS
|
||||
#endif
|
||||
_location->begin();
|
||||
_location->reset();
|
||||
|
||||
#ifndef PIN_GPS_EN
|
||||
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
|
||||
@@ -290,32 +459,113 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("No GPS detected");
|
||||
}
|
||||
#ifdef PIN_GPS_EN
|
||||
digitalWrite(PIN_GPS_EN, LOW); // Power off GPS until the setting is changed
|
||||
#endif
|
||||
_location->stop();
|
||||
gps_active = false; //Set GPS visibility off until setting is changed
|
||||
}
|
||||
|
||||
// gps code for rak might be moved to MicroNMEALoactionProvider
|
||||
// or make a new location provider ...
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
void EnvironmentSensorManager::rakGPSInit(){
|
||||
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
|
||||
#ifdef GPS_BAUD_RATE
|
||||
Serial1.begin(GPS_BAUD_RATE);
|
||||
#else
|
||||
Serial1.begin(9600);
|
||||
#endif
|
||||
|
||||
//search for the correct IO standby pin depending on socket used
|
||||
if(gpsIsAwake(WB_IO2)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
|
||||
}
|
||||
else if(gpsIsAwake(WB_IO4)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
|
||||
}
|
||||
else if(gpsIsAwake(WB_IO5)){
|
||||
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
|
||||
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
|
||||
}
|
||||
else{
|
||||
MESH_DEBUG_PRINTLN("No GPS found");
|
||||
gps_active = false;
|
||||
gps_detected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented
|
||||
//Now that GPS is found and set up, set to sleep for initial state
|
||||
stop_gps();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
|
||||
|
||||
//set initial waking state
|
||||
pinMode(ioPin,OUTPUT);
|
||||
digitalWrite(ioPin,LOW);
|
||||
delay(500);
|
||||
digitalWrite(ioPin,HIGH);
|
||||
delay(500);
|
||||
|
||||
//Try to init RAK12500 on I2C
|
||||
if (ublox_GNSS.begin(Wire) == true){
|
||||
MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin);
|
||||
ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
|
||||
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
|
||||
gpsResetPin = ioPin;
|
||||
i2cGPSFlag = true;
|
||||
gps_active = true;
|
||||
gps_detected = true;
|
||||
return true;
|
||||
}
|
||||
else if(Serial1){
|
||||
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
|
||||
if(PIN_GPS_EN){
|
||||
gpsResetPin = PIN_GPS_EN;
|
||||
}
|
||||
serialGPSFlag = true;
|
||||
gps_active = true;
|
||||
gps_detected = true;
|
||||
return true;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void EnvironmentSensorManager::start_gps() {
|
||||
gps_active = true;
|
||||
#ifdef PIN_GPS_EN
|
||||
pinMode(PIN_GPS_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_EN, HIGH);
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
pinMode(gpsResetPin, OUTPUT);
|
||||
digitalWrite(gpsResetPin, HIGH);
|
||||
return;
|
||||
#endif
|
||||
|
||||
_location->begin();
|
||||
_location->reset();
|
||||
|
||||
#ifndef PIN_GPS_RESET
|
||||
MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged");
|
||||
#endif
|
||||
}
|
||||
|
||||
void EnvironmentSensorManager::stop_gps() {
|
||||
gps_active = false;
|
||||
#ifdef PIN_GPS_EN
|
||||
pinMode(PIN_GPS_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
pinMode(gpsResetPin, OUTPUT);
|
||||
digitalWrite(gpsResetPin, LOW);
|
||||
return;
|
||||
#endif
|
||||
|
||||
_location->stop();
|
||||
|
||||
#ifndef PIN_GPS_EN
|
||||
MESH_DEBUG_PRINTLN("Stop GPS is N/A on this board. Actual GPS state unchanged");
|
||||
#endif
|
||||
}
|
||||
|
||||
void EnvironmentSensorManager::loop() {
|
||||
@@ -324,12 +574,33 @@ void EnvironmentSensorManager::loop() {
|
||||
_location->loop();
|
||||
|
||||
if (millis() > next_gps_update) {
|
||||
if (gps_active && _location->isValid()) {
|
||||
if(gps_active){
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
if(i2cGPSFlag){
|
||||
node_lat = ((double)ublox_GNSS.getLatitude())/10000000.;
|
||||
node_lon = ((double)ublox_GNSS.getLongitude())/10000000.;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
node_altitude = ((double)ublox_GNSS.getAltitude()) / 1000.0;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
|
||||
}
|
||||
else if (serialGPSFlag && _location->isValid()) {
|
||||
node_lat = ((double)_location->getLatitude())/1000000.;
|
||||
node_lon = ((double)_location->getLongitude())/1000000.;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
node_altitude = ((double)_location->getAltitude()) / 1000.0;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
|
||||
}
|
||||
#else
|
||||
if (_location->isValid()) {
|
||||
node_lat = ((double)_location->getLatitude())/1000000.;
|
||||
node_lon = ((double)_location->getLongitude())/1000000.;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
|
||||
node_altitude = ((double)_location->getAltitude()) / 1000.0;
|
||||
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
next_gps_update = millis() + 1000;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -13,8 +13,13 @@ protected:
|
||||
bool BMP280_initialized = false;
|
||||
bool INA3221_initialized = false;
|
||||
bool INA219_initialized = false;
|
||||
bool INA260_initialized = false;
|
||||
bool INA226_initialized = false;
|
||||
bool SHTC3_initialized = false;
|
||||
bool LPS22HB_initialized = false;
|
||||
bool MLX90614_initialized = false;
|
||||
bool VL53L0X_initialized = false;
|
||||
bool SHT4X_initialized = false;
|
||||
|
||||
bool gps_detected = false;
|
||||
bool gps_active = false;
|
||||
@@ -24,6 +29,10 @@ protected:
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
void initBasicGPS();
|
||||
#ifdef RAK_BOARD
|
||||
void rakGPSInit();
|
||||
bool gpsIsAwake(uint8_t ioPin);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
223
src/helpers/sensors/LPPDataHelpers.h
Normal file
223
src/helpers/sensors/LPPDataHelpers.h
Normal file
@@ -0,0 +1,223 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define LPP_DIGITAL_INPUT 0 // 1 byte
|
||||
#define LPP_DIGITAL_OUTPUT 1 // 1 byte
|
||||
#define LPP_ANALOG_INPUT 2 // 2 bytes, 0.01 signed
|
||||
#define LPP_ANALOG_OUTPUT 3 // 2 bytes, 0.01 signed
|
||||
#define LPP_GENERIC_SENSOR 100 // 4 bytes, unsigned
|
||||
#define LPP_LUMINOSITY 101 // 2 bytes, 1 lux unsigned
|
||||
#define LPP_PRESENCE 102 // 1 byte, bool
|
||||
#define LPP_TEMPERATURE 103 // 2 bytes, 0.1°C signed
|
||||
#define LPP_RELATIVE_HUMIDITY 104 // 1 byte, 0.5% unsigned
|
||||
#define LPP_ACCELEROMETER 113 // 2 bytes per axis, 0.001G
|
||||
#define LPP_BAROMETRIC_PRESSURE 115 // 2 bytes 0.1hPa unsigned
|
||||
#define LPP_VOLTAGE 116 // 2 bytes 0.01V unsigned
|
||||
#define LPP_CURRENT 117 // 2 bytes 0.001A unsigned
|
||||
#define LPP_FREQUENCY 118 // 4 bytes 1Hz unsigned
|
||||
#define LPP_PERCENTAGE 120 // 1 byte 1-100% unsigned
|
||||
#define LPP_ALTITUDE 121 // 2 byte 1m signed
|
||||
#define LPP_CONCENTRATION 125 // 2 bytes, 1 ppm unsigned
|
||||
#define LPP_POWER 128 // 2 byte, 1W, unsigned
|
||||
#define LPP_DISTANCE 130 // 4 byte, 0.001m, unsigned
|
||||
#define LPP_ENERGY 131 // 4 byte, 0.001kWh, unsigned
|
||||
#define LPP_DIRECTION 132 // 2 bytes, 1deg, unsigned
|
||||
#define LPP_UNIXTIME 133 // 4 bytes, unsigned
|
||||
#define LPP_GYROMETER 134 // 2 bytes per axis, 0.01 °/s
|
||||
#define LPP_COLOUR 135 // 1 byte per RGB Color
|
||||
#define LPP_GPS 136 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
||||
#define LPP_SWITCH 142 // 1 byte, 0/1
|
||||
#define LPP_POLYLINE 240 // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
||||
|
||||
// Multipliers
|
||||
#define LPP_DIGITAL_INPUT_MULT 1
|
||||
#define LPP_DIGITAL_OUTPUT_MULT 1
|
||||
#define LPP_ANALOG_INPUT_MULT 100
|
||||
#define LPP_ANALOG_OUTPUT_MULT 100
|
||||
#define LPP_GENERIC_SENSOR_MULT 1
|
||||
#define LPP_LUMINOSITY_MULT 1
|
||||
#define LPP_PRESENCE_MULT 1
|
||||
#define LPP_TEMPERATURE_MULT 10
|
||||
#define LPP_RELATIVE_HUMIDITY_MULT 2
|
||||
#define LPP_ACCELEROMETER_MULT 1000
|
||||
#define LPP_BAROMETRIC_PRESSURE_MULT 10
|
||||
#define LPP_VOLTAGE_MULT 100
|
||||
#define LPP_CURRENT_MULT 1000
|
||||
#define LPP_FREQUENCY_MULT 1
|
||||
#define LPP_PERCENTAGE_MULT 1
|
||||
#define LPP_ALTITUDE_MULT 1
|
||||
#define LPP_POWER_MULT 1
|
||||
#define LPP_DISTANCE_MULT 1000
|
||||
#define LPP_ENERGY_MULT 1000
|
||||
#define LPP_DIRECTION_MULT 1
|
||||
#define LPP_UNIXTIME_MULT 1
|
||||
#define LPP_GYROMETER_MULT 100
|
||||
#define LPP_GPS_LAT_LON_MULT 10000
|
||||
#define LPP_GPS_ALT_MULT 100
|
||||
#define LPP_SWITCH_MULT 1
|
||||
#define LPP_CONCENTRATION_MULT 1
|
||||
#define LPP_COLOUR_MULT 1
|
||||
|
||||
#define LPP_ERROR_OK 0
|
||||
#define LPP_ERROR_OVERFLOW 1
|
||||
#define LPP_ERROR_UNKOWN_TYPE 2
|
||||
|
||||
class LPPReader {
|
||||
const uint8_t* _buf;
|
||||
uint8_t _len;
|
||||
uint8_t _pos;
|
||||
|
||||
float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
uint32_t value = 0;
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
value = (value << 8) + buffer[i];
|
||||
}
|
||||
|
||||
int sign = 1;
|
||||
if (is_signed) {
|
||||
uint32_t bit = 1ul << ((size * 8) - 1);
|
||||
if ((value & bit) == bit) {
|
||||
value = (bit << 1) - value;
|
||||
sign = -1;
|
||||
}
|
||||
}
|
||||
return sign * ((float) value / multiplier);
|
||||
}
|
||||
|
||||
public:
|
||||
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
||||
|
||||
void reset() {
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
bool readHeader(uint8_t& channel, uint8_t& type) {
|
||||
if (_pos + 2 < _len) {
|
||||
channel = _buf[_pos++];
|
||||
type = _buf[_pos++];
|
||||
|
||||
return channel != 0; // channel 0 is End-of-data
|
||||
}
|
||||
return false; // end-of-buffer
|
||||
}
|
||||
|
||||
bool readGPS(float& lat, float& lon, float& alt) {
|
||||
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readVoltage(float& voltage) {
|
||||
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readCurrent(float& amps) {
|
||||
amps = getFloat(&_buf[_pos], 2, 1000, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readPower(float& watts) {
|
||||
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readTemperature(float& degrees_c) {
|
||||
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readPressure(float& pa) {
|
||||
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readRelativeHumidity(float& pct) {
|
||||
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readAltitude(float& m) {
|
||||
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
|
||||
void skipData(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
_pos += 9; break;
|
||||
case LPP_POLYLINE:
|
||||
_pos += 8; break; // TODO: this is MINIMIUM
|
||||
case LPP_GYROMETER:
|
||||
case LPP_ACCELEROMETER:
|
||||
_pos += 6; break;
|
||||
case LPP_GENERIC_SENSOR:
|
||||
case LPP_FREQUENCY:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
case LPP_UNIXTIME:
|
||||
_pos += 4; break;
|
||||
case LPP_COLOUR:
|
||||
_pos += 3; break;
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
case LPP_LUMINOSITY:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
case LPP_DIRECTION:
|
||||
case LPP_POWER:
|
||||
_pos += 2; break;
|
||||
default:
|
||||
_pos++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class LPPWriter {
|
||||
uint8_t* _buf;
|
||||
uint8_t _max_len;
|
||||
uint8_t _len;
|
||||
|
||||
void write(uint16_t value) {
|
||||
_buf[_len++] = (value >> 8) & 0xFF; // MSB
|
||||
_buf[_len++] = value & 0xFF; // LSB
|
||||
}
|
||||
|
||||
public:
|
||||
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
|
||||
|
||||
bool writeVoltage(uint8_t channel, float voltage) {
|
||||
if (_len + 4 <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = LPP_VOLTAGE;
|
||||
uint16_t value = voltage * 100;
|
||||
write(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeGPS(uint8_t channel, float lat, float lon, float alt) {
|
||||
if (_len + 11 <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = LPP_GPS;
|
||||
|
||||
int32_t lati = lat * 10000; // we lose some precision :-(
|
||||
int32_t loni = lon * 10000;
|
||||
int32_t alti = alt * 100;
|
||||
|
||||
_buf[_len++] = lati >> 16;
|
||||
_buf[_len++] = lati >> 8;
|
||||
_buf[_len++] = lati;
|
||||
_buf[_len++] = loni >> 16;
|
||||
_buf[_len++] = loni >> 8;
|
||||
_buf[_len++] = loni;
|
||||
_buf[_len++] = alti >> 16;
|
||||
_buf[_len++] = alti >> 8;
|
||||
_buf[_len++] = alti;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t length() { return _len; }
|
||||
};
|
||||
@@ -5,15 +5,31 @@
|
||||
#include <RTClib.h>
|
||||
|
||||
#ifndef GPS_EN
|
||||
#define GPS_EN (-1)
|
||||
#ifdef PIN_GPS_EN
|
||||
#define GPS_EN PIN_GPS_EN
|
||||
#else
|
||||
#define GPS_EN (-1)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef PIN_GPS_EN_ACTIVE
|
||||
#define PIN_GPS_EN_ACTIVE HIGH
|
||||
#endif
|
||||
|
||||
#ifndef GPS_RESET
|
||||
#define GPS_RESET (-1)
|
||||
#ifdef PIN_GPS_RESET
|
||||
#define GPS_RESET PIN_GPS_RESET
|
||||
#else
|
||||
#define GPS_RESET (-1)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef GPS_RESET_FORCE
|
||||
#define GPS_RESET_FORCE LOW
|
||||
#ifdef PIN_GPS_RESET_ACTIVE
|
||||
#define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE
|
||||
#else
|
||||
#define GPS_RESET_FORCE LOW
|
||||
#endif
|
||||
#endif
|
||||
|
||||
class MicroNMEALocationProvider : public LocationProvider {
|
||||
@@ -40,25 +56,25 @@ public :
|
||||
}
|
||||
|
||||
void begin() override {
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE);
|
||||
}
|
||||
if (_pin_reset != -1) {
|
||||
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
|
||||
}
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() override {
|
||||
if (_pin_reset != -1) {
|
||||
digitalWrite(_pin_reset, GPS_RESET_FORCE);
|
||||
delay(100);
|
||||
delay(10);
|
||||
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
if (_pin_en != -1) {
|
||||
digitalWrite(_pin_en, LOW);
|
||||
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,4 +123,4 @@ public :
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
class DisplayDriver {
|
||||
int _w, _h;
|
||||
@@ -21,9 +22,70 @@ public:
|
||||
virtual void setColor(Color c) = 0;
|
||||
virtual void setCursor(int x, int y) = 0;
|
||||
virtual void print(const char* str) = 0;
|
||||
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
|
||||
virtual void fillRect(int x, int y, int w, int h) = 0;
|
||||
virtual void drawRect(int x, int y, int w, int h) = 0;
|
||||
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
|
||||
virtual uint16_t getTextWidth(const char* str) = 0;
|
||||
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
|
||||
int w = getTextWidth(str);
|
||||
setCursor(mid_x - w/2, y);
|
||||
print(str);
|
||||
}
|
||||
|
||||
// convert UTF-8 characters to displayable block characters for compatibility
|
||||
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c >= 32 && c <= 126) {
|
||||
dest[j++] = c; // ASCII printable
|
||||
} else if (c >= 0x80) {
|
||||
dest[j++] = '\xDB'; // CP437 full block █
|
||||
while (src[i+1] && (src[i+1] & 0xC0) == 0x80)
|
||||
i++; // skip UTF-8 continuation bytes
|
||||
}
|
||||
}
|
||||
dest[j] = 0;
|
||||
}
|
||||
|
||||
// draw text with ellipsis if it exceeds max_width
|
||||
virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) {
|
||||
char temp_str[256]; // reasonable buffer size
|
||||
size_t len = strlen(str);
|
||||
if (len >= sizeof(temp_str)) len = sizeof(temp_str) - 1;
|
||||
memcpy(temp_str, str, len);
|
||||
temp_str[len] = 0;
|
||||
|
||||
if (getTextWidth(temp_str) <= max_width) {
|
||||
setCursor(x, y);
|
||||
print(temp_str);
|
||||
return;
|
||||
}
|
||||
|
||||
// for variable-width fonts (GxEPD), add space after ellipsis
|
||||
// for fixed-width fonts (OLED), keep tight spacing to save precious characters
|
||||
const char* ellipsis;
|
||||
// use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width
|
||||
int i_width = getTextWidth("i");
|
||||
int l_width = getTextWidth("l");
|
||||
if (i_width != l_width) {
|
||||
ellipsis = "... "; // variable-width fonts: add space
|
||||
} else {
|
||||
ellipsis = "..."; // fixed-width fonts: no space
|
||||
}
|
||||
|
||||
int ellipsis_width = getTextWidth(ellipsis);
|
||||
int str_len = strlen(temp_str);
|
||||
|
||||
while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) {
|
||||
temp_str[--str_len] = 0;
|
||||
}
|
||||
strcat(temp_str, ellipsis);
|
||||
|
||||
setCursor(x, y);
|
||||
print(temp_str);
|
||||
}
|
||||
|
||||
virtual void endFrame() = 0;
|
||||
};
|
||||
|
||||
@@ -2,20 +2,58 @@
|
||||
|
||||
#include "../../MeshCore.h"
|
||||
|
||||
BaseDisplay* E213Display::detectEInk()
|
||||
{
|
||||
// Test 1: Logic of BUSY pin
|
||||
|
||||
// Determines controller IC manufacturer
|
||||
// Fitipower: busy when LOW
|
||||
// Solomon Systech: busy when HIGH
|
||||
|
||||
// Force display BUSY by holding reset pin active
|
||||
pinMode(DISP_RST, OUTPUT);
|
||||
digitalWrite(DISP_RST, LOW);
|
||||
|
||||
delay(10);
|
||||
|
||||
// Read whether pin is HIGH or LOW while busy
|
||||
pinMode(DISP_BUSY, INPUT);
|
||||
bool busyLogic = digitalRead(DISP_BUSY);
|
||||
|
||||
// Test complete. Release pin
|
||||
pinMode(DISP_RST, INPUT);
|
||||
|
||||
if (busyLogic == LOW) {
|
||||
#ifdef VISION_MASTER_E213
|
||||
return new EInkDisplay_VisionMasterE213 ;
|
||||
#else
|
||||
return new EInkDisplay_WirelessPaperV1_1 ;
|
||||
#endif
|
||||
} else {// busy HIGH
|
||||
#ifdef VISION_MASTER_E213
|
||||
return new EInkDisplay_VisionMasterE213V1_1 ;
|
||||
#else
|
||||
return new EInkDisplay_WirelessPaperV1_1_1 ;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool E213Display::begin() {
|
||||
if (_init) return true;
|
||||
|
||||
powerOn();
|
||||
display.begin();
|
||||
|
||||
if(display==NULL) {
|
||||
display = detectEInk();
|
||||
}
|
||||
display->begin();
|
||||
// Set to landscape mode rotated 180 degrees
|
||||
display.setRotation(3);
|
||||
display->setRotation(3);
|
||||
|
||||
_init = true;
|
||||
_isOn = true;
|
||||
|
||||
clear();
|
||||
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
|
||||
display->fastmodeOn(); // Enable fast mode for quicker (partial) updates
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -23,15 +61,23 @@ bool E213Display::begin() {
|
||||
void E213Display::powerOn() {
|
||||
#ifdef PIN_VEXT_EN
|
||||
pinMode(PIN_VEXT_EN, OUTPUT);
|
||||
#ifdef PIN_VEXT_EN_ACTIVE
|
||||
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
|
||||
#else
|
||||
digitalWrite(PIN_VEXT_EN, LOW); // Active low
|
||||
#endif
|
||||
delay(50); // Allow power to stabilize
|
||||
#endif
|
||||
}
|
||||
|
||||
void E213Display::powerOff() {
|
||||
#ifdef PIN_VEXT_EN
|
||||
#ifdef PIN_VEXT_EN_ACTIVE
|
||||
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE);
|
||||
#else
|
||||
digitalWrite(PIN_VEXT_EN, HIGH); // Turn off power
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void E213Display::turnOn() {
|
||||
@@ -46,21 +92,23 @@ void E213Display::turnOff() {
|
||||
}
|
||||
|
||||
void E213Display::clear() {
|
||||
display.clear();
|
||||
display->clear();
|
||||
|
||||
}
|
||||
|
||||
void E213Display::startFrame(Color bkg) {
|
||||
// Fill screen with white first to ensure clean background
|
||||
display.fillRect(0, 0, width(), height(), WHITE);
|
||||
display->fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
if (bkg == LIGHT) {
|
||||
// Fill with black if light background requested (inverted for e-ink)
|
||||
display.fillRect(0, 0, width(), height(), BLACK);
|
||||
display->fillRect(0, 0, width(), height(), BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
void E213Display::setTextSize(int sz) {
|
||||
// The library handles text size internally
|
||||
display.setTextSize(sz);
|
||||
display->setTextSize(sz);
|
||||
}
|
||||
|
||||
void E213Display::setColor(Color c) {
|
||||
@@ -68,19 +116,19 @@ void E213Display::setColor(Color c) {
|
||||
}
|
||||
|
||||
void E213Display::setCursor(int x, int y) {
|
||||
display.setCursor(x, y);
|
||||
display->setCursor(x, y);
|
||||
}
|
||||
|
||||
void E213Display::print(const char *str) {
|
||||
display.print(str);
|
||||
display->print(str);
|
||||
}
|
||||
|
||||
void E213Display::fillRect(int x, int y, int w, int h) {
|
||||
display.fillRect(x, y, w, h, BLACK);
|
||||
display->fillRect(x, y, w, h, BLACK);
|
||||
}
|
||||
|
||||
void E213Display::drawRect(int x, int y, int w, int h) {
|
||||
display.drawRect(x, y, w, h, BLACK);
|
||||
display->drawRect(x, y, w, h, BLACK);
|
||||
}
|
||||
|
||||
void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
|
||||
@@ -98,7 +146,7 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
|
||||
|
||||
// If the bit is set, draw the pixel
|
||||
if (bitSet) {
|
||||
display.drawPixel(x + bx, y + by, BLACK);
|
||||
display->drawPixel(x + bx, y + by, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,10 +155,10 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
|
||||
uint16_t E213Display::getTextWidth(const char *str) {
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
display->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
void E213Display::endFrame() {
|
||||
display.update();
|
||||
display->update();
|
||||
}
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
|
||||
// Display driver for E213 e-ink display
|
||||
class E213Display : public DisplayDriver {
|
||||
EInkDisplay_VisionMasterE213 display;
|
||||
BaseDisplay* display=NULL;
|
||||
bool _init = false;
|
||||
bool _isOn = false;
|
||||
|
||||
public:
|
||||
E213Display() : DisplayDriver(250, 122) {}
|
||||
|
||||
~E213Display(){
|
||||
if(display!=NULL) {
|
||||
delete display;
|
||||
}
|
||||
}
|
||||
bool begin();
|
||||
bool isOn() override { return _isOn; }
|
||||
void turnOn() override;
|
||||
@@ -32,6 +36,7 @@ public:
|
||||
void endFrame() override;
|
||||
|
||||
private:
|
||||
BaseDisplay* detectEInk();
|
||||
void powerOn();
|
||||
void powerOff();
|
||||
};
|
||||
116
src/helpers/ui/E290Display.cpp
Normal file
116
src/helpers/ui/E290Display.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "E290Display.h"
|
||||
|
||||
#include "../../MeshCore.h"
|
||||
|
||||
bool E290Display::begin() {
|
||||
if (_init) return true;
|
||||
|
||||
powerOn();
|
||||
display.begin();
|
||||
|
||||
// Set to landscape mode rotated 180 degrees
|
||||
display.setRotation(3);
|
||||
|
||||
_init = true;
|
||||
_isOn = true;
|
||||
|
||||
clear();
|
||||
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void E290Display::powerOn() {
|
||||
#ifdef PIN_VEXT_EN
|
||||
pinMode(PIN_VEXT_EN, OUTPUT);
|
||||
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
|
||||
delay(50); // Allow power to stabilize
|
||||
#endif
|
||||
}
|
||||
|
||||
void E290Display::powerOff() {
|
||||
#ifdef PIN_VEXT_EN
|
||||
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // Turn off power
|
||||
#endif
|
||||
}
|
||||
|
||||
void E290Display::turnOn() {
|
||||
if (!_init) begin();
|
||||
powerOn();
|
||||
_isOn = true;
|
||||
}
|
||||
|
||||
void E290Display::turnOff() {
|
||||
powerOff();
|
||||
_isOn = false;
|
||||
}
|
||||
|
||||
void E290Display::clear() {
|
||||
display.clear();
|
||||
}
|
||||
|
||||
void E290Display::startFrame(Color bkg) {
|
||||
// Fill screen with white first to ensure clean background
|
||||
display.fillRect(0, 0, width(), height(), WHITE);
|
||||
if (bkg == LIGHT) {
|
||||
// Fill with black if light background requested (inverted for e-ink)
|
||||
display.fillRect(0, 0, width(), height(), BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
void E290Display::setTextSize(int sz) {
|
||||
// The library handles text size internally
|
||||
display.setTextSize(sz);
|
||||
}
|
||||
|
||||
void E290Display::setColor(Color c) {
|
||||
// implemented in individual display methods
|
||||
}
|
||||
|
||||
void E290Display::setCursor(int x, int y) {
|
||||
display.setCursor(x, y);
|
||||
}
|
||||
|
||||
void E290Display::print(const char *str) {
|
||||
display.print(str);
|
||||
}
|
||||
|
||||
void E290Display::fillRect(int x, int y, int w, int h) {
|
||||
display.fillRect(x, y, w, h, BLACK);
|
||||
}
|
||||
|
||||
void E290Display::drawRect(int x, int y, int w, int h) {
|
||||
display.drawRect(x, y, w, h, BLACK);
|
||||
}
|
||||
|
||||
void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
|
||||
// Width in bytes for bitmap processing
|
||||
uint16_t widthInBytes = (w + 7) / 8;
|
||||
|
||||
// Process the bitmap row by row
|
||||
for (int by = 0; by < h; by++) {
|
||||
// Scan across the row bit by bit
|
||||
for (int bx = 0; bx < w; bx++) {
|
||||
// Get the current bit using MSB ordering (like GxEPDDisplay)
|
||||
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
||||
uint8_t bitMask = 0x80 >> (bx & 7);
|
||||
bool bitSet = bits[byteOffset] & bitMask;
|
||||
|
||||
// If the bit is set, draw the pixel
|
||||
if (bitSet) {
|
||||
display.drawPixel(x + bx, y + by, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t E290Display::getTextWidth(const char *str) {
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
void E290Display::endFrame() {
|
||||
display.update();
|
||||
}
|
||||
37
src/helpers/ui/E290Display.h
Normal file
37
src/helpers/ui/E290Display.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <heltec-eink-modules.h>
|
||||
|
||||
// Display driver for E290 e-ink display
|
||||
class E290Display : public DisplayDriver {
|
||||
EInkDisplay_VisionMasterE290 display;
|
||||
bool _init = false;
|
||||
bool _isOn = false;
|
||||
|
||||
public:
|
||||
E290Display() : DisplayDriver(296, 128) {}
|
||||
|
||||
bool begin();
|
||||
bool isOn() override { return _isOn; }
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
void startFrame(Color bkg = DARK) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(Color c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char *str) override;
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char *str) override;
|
||||
void endFrame() override;
|
||||
|
||||
private:
|
||||
void powerOn();
|
||||
void powerOff();
|
||||
};
|
||||
38
src/helpers/ui/GenericVibration.cpp
Normal file
38
src/helpers/ui/GenericVibration.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifdef PIN_VIBRATION
|
||||
#include "GenericVibration.h"
|
||||
|
||||
void GenericVibration::begin() {
|
||||
pinMode(PIN_VIBRATION, OUTPUT);
|
||||
digitalWrite(PIN_VIBRATION, LOW);
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
void GenericVibration::trigger() {
|
||||
duration = millis();
|
||||
digitalWrite(PIN_VIBRATION, HIGH);
|
||||
}
|
||||
|
||||
void GenericVibration::loop() {
|
||||
if (isVibrating()) {
|
||||
if ((millis() / 1000) % 2 == 0) {
|
||||
digitalWrite(PIN_VIBRATION, LOW);
|
||||
} else {
|
||||
digitalWrite(PIN_VIBRATION, HIGH);
|
||||
}
|
||||
|
||||
if (millis() - duration > VIBRATION_TIMEOUT) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool GenericVibration::isVibrating() {
|
||||
return duration > 0;
|
||||
}
|
||||
|
||||
void GenericVibration::stop() {
|
||||
duration = 0;
|
||||
digitalWrite(PIN_VIBRATION, LOW);
|
||||
}
|
||||
|
||||
#endif // ifdef PIN_VIBRATION
|
||||
33
src/helpers/ui/GenericVibration.h
Normal file
33
src/helpers/ui/GenericVibration.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef PIN_VIBRATION
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/*
|
||||
* Vibration motor control class
|
||||
*
|
||||
* Provides vibration feedback for events like new messages and new contacts
|
||||
* Features:
|
||||
* - 1-second vibration pulse
|
||||
* - 5-second nag timeout (cooldown between vibrations)
|
||||
* - Non-blocking operation
|
||||
*/
|
||||
|
||||
#ifndef VIBRATION_TIMEOUT
|
||||
#define VIBRATION_TIMEOUT 5000 // 5 seconds default
|
||||
#endif
|
||||
|
||||
class GenericVibration {
|
||||
public:
|
||||
void begin(); // set up vibration pin
|
||||
void trigger(); // trigger vibration if cooldown has passed
|
||||
void loop(); // non-blocking timer handling
|
||||
bool isVibrating(); // returns true if currently vibrating
|
||||
void stop(); // stop vibration immediately
|
||||
|
||||
private:
|
||||
unsigned long duration;
|
||||
};
|
||||
|
||||
#endif // ifdef PIN_VIBRATION
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user