forked from iarv/meshing-around
Compare commits
623 Commits
v1.4.8hotf
...
v1.7.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
409d07436e | ||
|
|
5ab0001f2b | ||
|
|
5e34537af7 | ||
|
|
1764bdf4f3 | ||
|
|
2290f07351 | ||
|
|
ee01051cf7 | ||
|
|
de50a52fa6 | ||
|
|
8eabfaa9c4 | ||
|
|
ca7114b058 | ||
|
|
8b94dc8111 | ||
|
|
5b26aabb00 | ||
|
|
67b3c67348 | ||
|
|
860cceec59 | ||
|
|
53a0535e55 | ||
|
|
621f4ad916 | ||
|
|
118857ec15 | ||
|
|
1be13be92a | ||
|
|
895fc3fd37 | ||
|
|
0e0bda60ad | ||
|
|
903767f4b3 | ||
|
|
f54d362ea0 | ||
|
|
60bb68c6b5 | ||
|
|
feb9a1d9b3 | ||
|
|
d055c35c96 | ||
|
|
27820daaf4 | ||
|
|
56e8e1c0d5 | ||
|
|
4545b8f4a4 | ||
|
|
6ed48d49ce | ||
|
|
a3a54b081d | ||
|
|
ab420af63e | ||
|
|
a55c61c47d | ||
|
|
7236f47eb7 | ||
|
|
05e11ae5f8 | ||
|
|
f8ffcc19b1 | ||
|
|
ea20eec604 | ||
|
|
d1204d2c26 | ||
|
|
654d8b3ff7 | ||
|
|
3bf12d62b5 | ||
|
|
0ec8613d27 | ||
|
|
10dd413ae7 | ||
|
|
09ac7525b3 | ||
|
|
aac497dfa0 | ||
|
|
6f652230b0 | ||
|
|
6f1c44e62a | ||
|
|
837d049acb | ||
|
|
2463407ade | ||
|
|
af2bc7be0c | ||
|
|
38654213e8 | ||
|
|
a06819dbda | ||
|
|
9818cccbbf | ||
|
|
239dbb8be0 | ||
|
|
872a9601d0 | ||
|
|
2b6dc726e1 | ||
|
|
ef27ddff84 | ||
|
|
8a8ad961d5 | ||
|
|
a8b4362d3c | ||
|
|
dc731ae237 | ||
|
|
d0d024d770 | ||
|
|
9b633502e6 | ||
|
|
ac1a007ba4 | ||
|
|
09cf6f585c | ||
|
|
916719f1c5 | ||
|
|
11a6dc3cf0 | ||
|
|
c160678e79 | ||
|
|
0c9fd919ab | ||
|
|
e17dc79896 | ||
|
|
06d6855d92 | ||
|
|
66f937a645 | ||
|
|
f4985b744a | ||
|
|
7ae6174f96 | ||
|
|
d44fdd4462 | ||
|
|
3dd6da4684 | ||
|
|
a229b57964 | ||
|
|
5e045b6447 | ||
|
|
1e328d4f4d | ||
|
|
879d141844 | ||
|
|
7daf8c4c33 | ||
|
|
3e6d1f5c6f | ||
|
|
32deea9e3b | ||
|
|
793fabcdb8 | ||
|
|
a7a710208a | ||
|
|
41efbc6189 | ||
|
|
f399190d3c | ||
|
|
5760c10534 | ||
|
|
9deb4a9436 | ||
|
|
1f348d963d | ||
|
|
b35edf13c8 | ||
|
|
37185b9f8b | ||
|
|
4e25535ede | ||
|
|
4de2a36099 | ||
|
|
6c0d6fd343 | ||
|
|
abd865c918 | ||
|
|
82222addbe | ||
|
|
7750ce468b | ||
|
|
135778d511 | ||
|
|
c54df673c3 | ||
|
|
2fec08060f | ||
|
|
ce9af3c0d3 | ||
|
|
217cd01d0a | ||
|
|
8a6057995b | ||
|
|
47e21dbaab | ||
|
|
267f50c591 | ||
|
|
0013a7bb74 | ||
|
|
73fe8be432 | ||
|
|
3d45195ae9 | ||
|
|
ff390cf470 | ||
|
|
17d8cd1067 | ||
|
|
b9348c906d | ||
|
|
6ba3508cc5 | ||
|
|
1c78f154da | ||
|
|
e0a3d0f94e | ||
|
|
066211e9f2 | ||
|
|
5701cd108b | ||
|
|
b877a294ac | ||
|
|
2aedcfc46e | ||
|
|
12147db5d0 | ||
|
|
cef37b574b | ||
|
|
6f121b7aac | ||
|
|
9e31b7f47e | ||
|
|
f3103984ef | ||
|
|
9c8b3f0a54 | ||
|
|
f88cbf210e | ||
|
|
9909113beb | ||
|
|
c1b783b1cd | ||
|
|
9b3b6a5d3d | ||
|
|
cffdb3c089 | ||
|
|
7bb9c9ac55 | ||
|
|
830ec95080 | ||
|
|
0ea575ac70 | ||
|
|
d836255716 | ||
|
|
4f115c9c21 | ||
|
|
63bd5b836d | ||
|
|
5ad9b9a261 | ||
|
|
7a024b681f | ||
|
|
75df5a695b | ||
|
|
0ef8cffd56 | ||
|
|
73e8e063d2 | ||
|
|
82880677f4 | ||
|
|
fe8ba8aaf4 | ||
|
|
cea9147745 | ||
|
|
c1c68d4c10 | ||
|
|
5fcd21680e | ||
|
|
9e1356172f | ||
|
|
de7fdfad11 | ||
|
|
a87055874a | ||
|
|
5c7433091d | ||
|
|
f0ca818461 | ||
|
|
76006dcda7 | ||
|
|
33abe646ae | ||
|
|
c47004c47c | ||
|
|
e66d945be7 | ||
|
|
10afc128f4 | ||
|
|
e6fc794951 | ||
|
|
4839e9ba03 | ||
|
|
bde15e311a | ||
|
|
21c83222e9 | ||
|
|
bbcdd6656a | ||
|
|
7f61b86252 | ||
|
|
25ae27a162 | ||
|
|
a04133e82f | ||
|
|
2a9dfc90ee | ||
|
|
f1bf84f6f0 | ||
|
|
4b91ef10b4 | ||
|
|
cd4497b129 | ||
|
|
01374a8307 | ||
|
|
46c115b783 | ||
|
|
eec7230a84 | ||
|
|
9394fd6ca9 | ||
|
|
c6653da1f3 | ||
|
|
9f47958a03 | ||
|
|
78e51b7be1 | ||
|
|
26fcf6fc02 | ||
|
|
c2336850fe | ||
|
|
54e0d17e70 | ||
|
|
7a6d1f7b29 | ||
|
|
7e26d3f0e5 | ||
|
|
89be8e13a2 | ||
|
|
aa8482ab52 | ||
|
|
69605e0984 | ||
|
|
8e15a3fc99 | ||
|
|
d671b19bce | ||
|
|
943dd4d5a3 | ||
|
|
05d8671b3f | ||
|
|
4bccd33827 | ||
|
|
71ebe7087f | ||
|
|
8dbffe2e63 | ||
|
|
cbea9b5294 | ||
|
|
acdc94cd06 | ||
|
|
e9deb62047 | ||
|
|
f1ad470f88 | ||
|
|
b19f7be0b0 | ||
|
|
053acd1ac6 | ||
|
|
3d5b671d81 | ||
|
|
f090230c96 | ||
|
|
d9040a4ec7 | ||
|
|
e35c954e5d | ||
|
|
93ed84fdee | ||
|
|
9f074e5250 | ||
|
|
12d94fb0dc | ||
|
|
afa2bc4024 | ||
|
|
8dcbf66618 | ||
|
|
902b4f22ee | ||
|
|
7ae0d5e927 | ||
|
|
49b8206e76 | ||
|
|
5a30cc7511 | ||
|
|
a85cc8c593 | ||
|
|
5ae496702d | ||
|
|
1dffa0987d | ||
|
|
f3d07eed97 | ||
|
|
de8266b955 | ||
|
|
d482f2ccc9 | ||
|
|
9f676a4c8d | ||
|
|
5d0dae236c | ||
|
|
bf32eca47d | ||
|
|
dcef6da5bc | ||
|
|
a1ffc8b1f6 | ||
|
|
921b66f9e1 | ||
|
|
0553a43a01 | ||
|
|
5079c67f62 | ||
|
|
785deb2add | ||
|
|
4b0654971c | ||
|
|
d2fd133743 | ||
|
|
d689495ee7 | ||
|
|
b16b4e3c12 | ||
|
|
10109672a7 | ||
|
|
4a3cd2560c | ||
|
|
576898b8fe | ||
|
|
4db9c136d6 | ||
|
|
a1a4c1b0f0 | ||
|
|
7b1b435e45 | ||
|
|
54e716d2cc | ||
|
|
b44fa22c11 | ||
|
|
5829cdcef9 | ||
|
|
f0a93b0191 | ||
|
|
9014a7e8f9 | ||
|
|
6c9f9f2521 | ||
|
|
9bae30bcb1 | ||
|
|
7069ba1f43 | ||
|
|
ae844f8ecd | ||
|
|
af734ccb1f | ||
|
|
1ff5895bad | ||
|
|
f12fa0fe9b | ||
|
|
45c67024e7 | ||
|
|
725cbd8045 | ||
|
|
502a4f2666 | ||
|
|
9aaebaad62 | ||
|
|
d163bffba6 | ||
|
|
36ba04a234 | ||
|
|
0ac683b5c0 | ||
|
|
b16d9322e3 | ||
|
|
868009b650 | ||
|
|
f917df709c | ||
|
|
ab54dc06d7 | ||
|
|
c7b7b182b9 | ||
|
|
b78cf4d022 | ||
|
|
6f492ef382 | ||
|
|
e24c9a9d56 | ||
|
|
b1155dea7d | ||
|
|
0d9245d448 | ||
|
|
858bef7703 | ||
|
|
acf39d0870 | ||
|
|
89a0884600 | ||
|
|
70e11117f1 | ||
|
|
d3f07ae524 | ||
|
|
4f9c36fdad | ||
|
|
df15fb54b0 | ||
|
|
638dc4df16 | ||
|
|
81e91ab6c5 | ||
|
|
05476c2bff | ||
|
|
3b4b0e8c32 | ||
|
|
772218d108 | ||
|
|
dae2e4c4f4 | ||
|
|
5d5595ef8b | ||
|
|
cf16fc3db7 | ||
|
|
70659c9c14 | ||
|
|
b04368f852 | ||
|
|
9e5285a845 | ||
|
|
475d475e18 | ||
|
|
2c4cfa9e81 | ||
|
|
15d7f75507 | ||
|
|
30131bc6d5 | ||
|
|
5373b61f83 | ||
|
|
7eb629676b | ||
|
|
db9b89d0ac | ||
|
|
d7af337a63 | ||
|
|
e3c5eb6add | ||
|
|
b0e57e8aca | ||
|
|
b4168214b6 | ||
|
|
7fa5928537 | ||
|
|
f12198b140 | ||
|
|
0d44ffb635 | ||
|
|
c11ebf1443 | ||
|
|
b94a5ebd8d | ||
|
|
3392d2d5a8 | ||
|
|
1df3a7aaa2 | ||
|
|
9a11214208 | ||
|
|
0a4f101370 | ||
|
|
5f3c32dc00 | ||
|
|
74cb135c6c | ||
|
|
a20e520501 | ||
|
|
23e0e4c6a0 | ||
|
|
10918546d6 | ||
|
|
cf16cc6606 | ||
|
|
3b73b665d6 | ||
|
|
993fd760af | ||
|
|
a029334576 | ||
|
|
eb8143f298 | ||
|
|
c756b447ac | ||
|
|
cef05e061c | ||
|
|
c85d517b91 | ||
|
|
170d1a6a45 | ||
|
|
8d2313cfb1 | ||
|
|
ed8636f5a5 | ||
|
|
b95d94f06f | ||
|
|
f7cdf446bf | ||
|
|
28e8e2705a | ||
|
|
9bc6f6f661 | ||
|
|
2630310210 | ||
|
|
3fae42305c | ||
|
|
9cc8dd7143 | ||
|
|
7ffa9d5309 | ||
|
|
30d2b996c0 | ||
|
|
49c098ef0b | ||
|
|
afa41c6ecd | ||
|
|
8861179cb2 | ||
|
|
f32ceb0383 | ||
|
|
9a380964aa | ||
|
|
180a8261ca | ||
|
|
0536657c8e | ||
|
|
c5a2330dd1 | ||
|
|
dc0b5be387 | ||
|
|
a1f43a5e94 | ||
|
|
b05a817769 | ||
|
|
f7187fdf27 | ||
|
|
cca51d68dd | ||
|
|
21804cc975 | ||
|
|
7a9ee27336 | ||
|
|
0c637226b2 | ||
|
|
555b14ddc0 | ||
|
|
656c23c631 | ||
|
|
bb591257c9 | ||
|
|
364a5c5c67 | ||
|
|
8cb05d38db | ||
|
|
f9fe13f322 | ||
|
|
b8d33cc270 | ||
|
|
a6ce9e9211 | ||
|
|
60bdabdd1b | ||
|
|
9c5c2080cf | ||
|
|
8f758229cb | ||
|
|
8ac9c53f1a | ||
|
|
98cbf5528c | ||
|
|
6296150677 | ||
|
|
13cb1e8df9 | ||
|
|
e26e876ccf | ||
|
|
550b50f74e | ||
|
|
ac5aa1a201 | ||
|
|
19700f54c5 | ||
|
|
7e5626cd30 | ||
|
|
c27b6ed8a1 | ||
|
|
717181bcd0 | ||
|
|
4d5916df29 | ||
|
|
93b7a1d613 | ||
|
|
35cc029984 | ||
|
|
589d44c152 | ||
|
|
06a14d875f | ||
|
|
454f823ad7 | ||
|
|
6974c4ef66 | ||
|
|
bd956dfebc | ||
|
|
4aaac5ba49 | ||
|
|
2ae792dd8d | ||
|
|
ca033f024e | ||
|
|
ad11f787de | ||
|
|
e3d1607c86 | ||
|
|
b68461cbc8 | ||
|
|
ddad35aa1e | ||
|
|
35f4aad6f8 | ||
|
|
f08f98e040 | ||
|
|
467376d9c7 | ||
|
|
1cbdc93632 | ||
|
|
2323015617 | ||
|
|
51de0dee8a | ||
|
|
b74c0ebd36 | ||
|
|
0a4c54a5a2 | ||
|
|
481809493c | ||
|
|
c3914e0423 | ||
|
|
ac40254bc4 | ||
|
|
b6540a1d20 | ||
|
|
87d29d123f | ||
|
|
0aa6f8cc07 | ||
|
|
e2bb480f5f | ||
|
|
920f951e47 | ||
|
|
215fe76f2a | ||
|
|
1740bbf666 | ||
|
|
f9370d47b4 | ||
|
|
91072cb47d | ||
|
|
c30be37f02 | ||
|
|
d51dadba04 | ||
|
|
99c404f479 | ||
|
|
659ee2959c | ||
|
|
1ac9f3b0d6 | ||
|
|
d0dc737863 | ||
|
|
e438c82a11 | ||
|
|
9d7d4601dc | ||
|
|
fdd741446c | ||
|
|
fdbab1685f | ||
|
|
ed0940b126 | ||
|
|
a087c7bb3a | ||
|
|
0439db2ec0 | ||
|
|
c1a5d4d336 | ||
|
|
eeffc6361a | ||
|
|
e2be3c20b7 | ||
|
|
b43c21fc98 | ||
|
|
e115f33d47 | ||
|
|
b8016aafc9 | ||
|
|
743b0ab10b | ||
|
|
e06b2a3581 | ||
|
|
582e00402a | ||
|
|
82551e0b4a | ||
|
|
a9c2660ec1 | ||
|
|
fa802ba313 | ||
|
|
874d56045e | ||
|
|
8204cbe60f | ||
|
|
a50c06206c | ||
|
|
895e5a2b07 | ||
|
|
2012986aff | ||
|
|
63d1f84887 | ||
|
|
d8233bc9e2 | ||
|
|
bdea3d6036 | ||
|
|
2fe2009b97 | ||
|
|
dcad12935f | ||
|
|
0e2f6343a2 | ||
|
|
56bd6f9ea7 | ||
|
|
5718a43d20 | ||
|
|
f759e2e7e5 | ||
|
|
1e97554cbf | ||
|
|
04d4a2f5a7 | ||
|
|
fb47756deb | ||
|
|
a33fed711d | ||
|
|
bcb741102d | ||
|
|
8b2d933fd1 | ||
|
|
f8d6419551 | ||
|
|
cf518aeff5 | ||
|
|
95eebcde2b | ||
|
|
5cd7dca9b0 | ||
|
|
eb87cf1bc8 | ||
|
|
8a510a7b11 | ||
|
|
e2631407e8 | ||
|
|
eb86fa911c | ||
|
|
448ad65c67 | ||
|
|
bb8d2167ce | ||
|
|
a2bf33d71d | ||
|
|
e287bdeaef | ||
|
|
16e5acbd27 | ||
|
|
1ea6961393 | ||
|
|
bd2bce0029 | ||
|
|
33c8d4c0ad | ||
|
|
d453c3cac1 | ||
|
|
187fc7c2e4 | ||
|
|
33154626e5 | ||
|
|
cfdbf1836f | ||
|
|
054692adf0 | ||
|
|
ce33421b16 | ||
|
|
d2cde424fc | ||
|
|
517ae5d4b4 | ||
|
|
e69ee5c1a8 | ||
|
|
b2eae85cc2 | ||
|
|
0749df04e5 | ||
|
|
a66ea58d24 | ||
|
|
13738d1042 | ||
|
|
695d510b9f | ||
|
|
f5e80c31b1 | ||
|
|
572a15fbab | ||
|
|
8dc9a5de3f | ||
|
|
c8643b7ce9 | ||
|
|
786dcab420 | ||
|
|
ab2f9a9846 | ||
|
|
daf43f306b | ||
|
|
53adb4be70 | ||
|
|
2458a4d141 | ||
|
|
1c78a8f593 | ||
|
|
6077eef26e | ||
|
|
8f3aaaba25 | ||
|
|
b1cd0ca44f | ||
|
|
879555915f | ||
|
|
f61ba7c1af | ||
|
|
7cb2ea33c7 | ||
|
|
855a9ac0d0 | ||
|
|
3e2e1de8ce | ||
|
|
372f49d6ef | ||
|
|
a31b3e1c79 | ||
|
|
d3ecef9216 | ||
|
|
1175e23525 | ||
|
|
08e3e21306 | ||
|
|
7e3de5e490 | ||
|
|
abc3eccf4e | ||
|
|
80751f9cfc | ||
|
|
7209992887 | ||
|
|
6c18d97f27 | ||
|
|
cdd7d6e766 | ||
|
|
8d5334126f | ||
|
|
bcd23ebb83 | ||
|
|
5d581c2319 | ||
|
|
8e3b449c42 | ||
|
|
0975b3235a | ||
|
|
9a2a4f1b77 | ||
|
|
5df17b5905 | ||
|
|
894c5f155f | ||
|
|
f848e12571 | ||
|
|
7adf6e7a1d | ||
|
|
c6958c7c69 | ||
|
|
05c6e56a4f | ||
|
|
c45cf5d207 | ||
|
|
a3995f7cce | ||
|
|
fb3652a954 | ||
|
|
b385001db2 | ||
|
|
ac96ca9e2f | ||
|
|
02ffe0eb3a | ||
|
|
389945e023 | ||
|
|
446fa0c049 | ||
|
|
8b4409c115 | ||
|
|
5684a75c65 | ||
|
|
1c6a98fea5 | ||
|
|
7c1b886c3d | ||
|
|
75bbd1a0cd | ||
|
|
a53f5a033b | ||
|
|
ea37405149 | ||
|
|
e16ecbe1b7 | ||
|
|
db6f20dd3b | ||
|
|
9fa60d0c84 | ||
|
|
2fdad79dbb | ||
|
|
20342fb58c | ||
|
|
b7e815cf85 | ||
|
|
8e3d1c432e | ||
|
|
1a8ed573a8 | ||
|
|
63516b36e4 | ||
|
|
d17b05a40a | ||
|
|
e4cefa2264 | ||
|
|
90bf3459c9 | ||
|
|
0983259117 | ||
|
|
377e5a9825 | ||
|
|
7edcb4457a | ||
|
|
3fec7867d9 | ||
|
|
7e447616d9 | ||
|
|
e59c3de0aa | ||
|
|
db808568cb | ||
|
|
0615733445 | ||
|
|
402c58c111 | ||
|
|
dde6c2ed32 | ||
|
|
766ff0a195 | ||
|
|
d614cbcff5 | ||
|
|
81798c1fc2 | ||
|
|
210a75671f | ||
|
|
f3e113dcc1 | ||
|
|
145664a42f | ||
|
|
acc770732e | ||
|
|
ded4c79911 | ||
|
|
ad0c9c710f | ||
|
|
259c4991f9 | ||
|
|
5fe185ab7f | ||
|
|
974caaff42 | ||
|
|
41d8758969 | ||
|
|
92e1e3168e | ||
|
|
a608e29911 | ||
|
|
015b72c8c6 | ||
|
|
74cf5841ff | ||
|
|
9ba7b1c972 | ||
|
|
5bf0417203 | ||
|
|
2b7a20f8d9 | ||
|
|
2afb49cbc7 | ||
|
|
17008b7711 | ||
|
|
36ff328380 | ||
|
|
bb051f4225 | ||
|
|
61c5be1a08 | ||
|
|
bc7d47b2a7 | ||
|
|
24bcd5cbf9 | ||
|
|
8407512b0f | ||
|
|
6f4e8615a3 | ||
|
|
314d36e0dc | ||
|
|
27accb0d4a | ||
|
|
fd84505ad1 | ||
|
|
8f75b13c4d | ||
|
|
31d05f8aa7 | ||
|
|
cdfe4bb844 | ||
|
|
f30e9cd8b8 | ||
|
|
931bc7b9f7 | ||
|
|
049c0d5ad7 | ||
|
|
a5f1e452e4 | ||
|
|
d89cd8598d | ||
|
|
d4e3ea60e3 | ||
|
|
b98bc8429a | ||
|
|
4bb7c9296a | ||
|
|
bb7b5b1c90 | ||
|
|
c400f6f998 | ||
|
|
fce6c0b2e4 | ||
|
|
0d0288ba18 | ||
|
|
c25d7bc8de | ||
|
|
d42fa72d54 | ||
|
|
bc7176c1cf | ||
|
|
15d454f93a | ||
|
|
249ee3bb5a | ||
|
|
a3b3d4ea0e | ||
|
|
27f9d04538 | ||
|
|
03f1869b23 | ||
|
|
479e177a64 | ||
|
|
5cf166af87 | ||
|
|
e24bcd7d38 | ||
|
|
768898df64 | ||
|
|
cf282e04bb | ||
|
|
db4edac083 | ||
|
|
877d0cf7f8 | ||
|
|
e78c441a6e | ||
|
|
e945819365 | ||
|
|
23e8db50fd | ||
|
|
193ffe6394 | ||
|
|
87016186d8 | ||
|
|
d7d96a89cf | ||
|
|
aa5ef23363 | ||
|
|
c18e0401e4 | ||
|
|
8568990295 | ||
|
|
44e6460224 | ||
|
|
d53480290c | ||
|
|
1499d883bc | ||
|
|
883a6902fa |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,7 +8,8 @@ config.ini
|
||||
venv/
|
||||
|
||||
# logs
|
||||
logs/*.log
|
||||
logs/
|
||||
install_notes.txt
|
||||
|
||||
# modified .service files
|
||||
etc/*.service
|
||||
@@ -18,3 +19,6 @@ __pycache__/
|
||||
|
||||
# rag data
|
||||
data/rag/*
|
||||
|
||||
# qrz db
|
||||
data/qrz.db
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
there is some ideas for adding code in modules/README.md
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,24 +1,21 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.13-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale default to en_US.UTF-8
|
||||
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
dpkg-reconfigure --frontend=noninteractive locales && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANG="en_US.UTF-8"
|
||||
ENV TZ="America/Los_Angeles"
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
COPY requirements.txt .
|
||||
COPY config.template /app/config.ini
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
COPY config.ini /app/config.ini
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/script/docker/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]
|
||||
|
||||
379
README.md
379
README.md
@@ -1,17 +1,24 @@
|
||||
# Mesh Bot for Network Testing and BBS Activities
|
||||
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
|
||||

|
||||
|
||||
## Key Features
|
||||

|
||||
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
|
||||
- **New Node Hello**: Greet new nodes on the mesh with a hello message
|
||||
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
### Network Tools
|
||||
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
|
||||
|
||||
### Multi Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
@@ -19,30 +26,42 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visability.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **Wiki Integration**: Look up data using Wikipedia results.
|
||||
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
|
||||
- **Satalite Pass Info**: Get passes for satalite at your location.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
|
||||
|
||||
### CheckList / Check In Out
|
||||
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Usefull for accountability of people, assets. Radio-Net, FEMA, Trailhead.
|
||||
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
|
||||
### Radio Frequency Monitoring
|
||||
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
|
||||
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
|
||||
|
||||
### NOAA EAS Alerts
|
||||
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
|
||||
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
|
||||
### EAS Alerts
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
- **News File**: On request of news, the contents of the file are returned.
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
@@ -51,35 +70,25 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Clone the Repository
|
||||
If you dont have git you will need it `sudo apt-get install git`
|
||||
```sh
|
||||
git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
The code is under active development, so make sure to pull the latest changes regularly!
|
||||
|
||||
#### Optional Automation of setup
|
||||
#### Quick setup
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
|
||||
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
#### Docker Installation
|
||||
If you prefer to use Docker, follow these steps:
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
|
||||
2. Build the Docker image:
|
||||
```sh
|
||||
cd meshing-around
|
||||
docker build -t meshing-around .
|
||||
```
|
||||
3. Run the Docker container:
|
||||
```sh
|
||||
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
|
||||
```
|
||||
|
||||
#### Custom Install
|
||||
#### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -90,8 +99,10 @@ Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
@@ -117,16 +128,19 @@ enabled = False
|
||||
```
|
||||
|
||||
### General Settings
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
|
||||
|
||||
```ini
|
||||
[general]
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
|
||||
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
@@ -134,10 +148,11 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
Modules can be enabled or disabled as needed.
|
||||
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
|
||||
|
||||
```ini
|
||||
[bbs]
|
||||
@@ -158,18 +173,78 @@ lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
|
||||
|
||||
### Sentry Settings
|
||||
|
||||
Sentry Bot detects anyone coming close to the bot-node.
|
||||
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
|
||||
|
||||
```ini
|
||||
SentryEnabled = True # detect anyone close to the bot
|
||||
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
|
||||
SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
highFlyingAlert = True # HighFlying Node alert
|
||||
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
|
||||
|
||||
```ini
|
||||
[smtp]
|
||||
# enable or disable the SMTP module, minimum required for outbound notifications
|
||||
enableSMTP = True # enable or disable the IMAP module for inbound email, not implimented yet
|
||||
enableImap = False # list of Sysop Emails seperate with commas, used only in emergemcy responder currently
|
||||
sysopEmails =
|
||||
# See config.template for all the SMTP settings
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
SMTP_AUTH = True
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
```
|
||||
|
||||
### Emergency Response Handler
|
||||
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
|
||||
|
||||
```ini
|
||||
[emergencyHandler]
|
||||
enabled = True # enable or disable the emergency response handler
|
||||
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
|
||||
alert_interface = 1
|
||||
```
|
||||
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
|
||||
|
||||
```ini
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = True
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
A repeater function for two different nodes and cross-posting messages. The [`repeater_channels`] is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
|
||||
```ini
|
||||
[repeater] # repeater module
|
||||
@@ -177,22 +252,8 @@ enabled = True
|
||||
repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### Radio Monitoring
|
||||
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
signalHoldTime = 10 # hold time for high SNR
|
||||
signalCooldown = 5 # the following are combined to reset the monitor
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
### Ollama (LLM/AI) Settings
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
@@ -209,31 +270,47 @@ llmEnableHistory = True # enable history for the LLM model to use in responses a
|
||||
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
```
|
||||
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
|
||||
|
||||
### Radio Monitoring
|
||||
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = True
|
||||
rigControlServerAddress = localhost:4532
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
signalHoldTime = 10 # hold time for high SNR
|
||||
signalCooldown = 5 # the following are combined to reset the monitor
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
### File Monitoring
|
||||
Some dev notes for ideas of use
|
||||
|
||||
```ini
|
||||
[fileMon]
|
||||
enabled = True
|
||||
filemon_enabled = True
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enables running of bash commands runShell.sh demo for sysinfo
|
||||
```
|
||||
#### NOAA EAS
|
||||
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
|
||||
|
||||
```ini
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
```
|
||||
#### Offline EAS
|
||||
|
||||
To Monitor EAS with no internet connection see the following notes
|
||||
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
|
||||
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
|
||||
|
||||
no examples yet for these tools
|
||||
|
||||
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
|
||||
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
|
||||
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
|
||||
- this can be used with a rtl-sdr to capture alerts
|
||||
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
|
||||
- [dsame3](https://github.com/jamieden/dsame3)
|
||||
- has a sample .ogg file for testing alerts
|
||||
|
||||
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
|
||||
@@ -245,8 +322,37 @@ The following example shell command will pipe rtl_sdr to alert.txt
|
||||
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
|
||||
```
|
||||
|
||||
#### Newspaper on mesh
|
||||
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
|
||||
|
||||
### Greet new nodes QRZ module
|
||||
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
|
||||
```ini
|
||||
[qrz]
|
||||
enabled = True # QRZ Hello to new nodes
|
||||
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
```
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -257,81 +363,50 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBL Link Brodcast message
|
||||
The scheduler also handles the BBS Link Brodcast message, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
```
|
||||
```ini
|
||||
bbslink_enabled = True
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
|
||||
### MQTT Notes
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install geopy
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
~~There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two~~
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack`, `test` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `test` | used to test the limits of data transfer `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details |
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) | |
|
||||
| `valert` | Returns USGS Volcano Data | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
@@ -342,27 +417,41 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
|
||||
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
|
||||
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
|
||||
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
|
||||
| `sms:` | Send sms-email to multiple address on file | |
|
||||
| `setemail`| Sets the email for easy communciations | |
|
||||
| `setsms` | Adds the SMS-Email for quick communications | |
|
||||
| `clearsms` | Clears all SMS-Emails on file for node | |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
|
||||
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
|
||||
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
|
||||
| `checklist` | Display the checklist database, with note | ✅ |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
|
||||
| `hangman` | Plays the classic word guess game | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
# Recognition
|
||||
|
||||
@@ -380,15 +469,57 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
|
||||
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
|
||||
- [Golf](https://github.com/danfriedman30/pythongame)
|
||||
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
### Special Thanks
|
||||
- **xdep**: For the reporting tools.
|
||||
- **Nestpebble**: For new ideas and enhancements.
|
||||
- **mrpatrick1991**: For Docker configurations.
|
||||
- **Mike O'Connell/skrrt**: For [text](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **dj505**: trying it on windows!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **trs2982**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.
|
||||
|
||||
210
config.template
210
config.template
@@ -8,84 +8,110 @@
|
||||
type = serial
|
||||
port = /dev/ttyACM0
|
||||
# port = /dev/ttyUSB0
|
||||
# port = COM1
|
||||
# hostname = 192.168.0.1
|
||||
# hostname = meshtastic.local
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support
|
||||
# Additional interface for multi radio support
|
||||
[interface2]
|
||||
enabled = False
|
||||
type = serial
|
||||
port = /dev/ttyUSB0
|
||||
#port = /dev/ttyACM1
|
||||
# port = /dev/ttyACM1
|
||||
# port = COM1
|
||||
# hostname = meshtastic.local
|
||||
# hostname = localhost
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# example, the third interface would be [interface3] up to 9
|
||||
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# Allows auto-ping feature in a channel, False forces to 1 ping only
|
||||
autoPingInChannel = False
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreDefaultChannel = False
|
||||
# ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
ignoreChannels =
|
||||
# require ! to be the first character in a command
|
||||
cmdBang = False
|
||||
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
|
||||
|
||||
# whoami
|
||||
whoami = True
|
||||
|
||||
# enable or disable the Joke module
|
||||
DadJokes = True
|
||||
DadJokesEmoji = False
|
||||
|
||||
# enable or disable the Solar module
|
||||
spaceWeather = True
|
||||
|
||||
# enable or disable the wikipedia search module
|
||||
wikipedia = True
|
||||
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = False
|
||||
# Ollama model to use (defaults to gemma2:2b)
|
||||
# ollamaModel = llama3.1
|
||||
# server instance to use (defaults to local machine install)
|
||||
ollamaHostName = http://localhost:11434
|
||||
# Produce LLM replies to messages that aren't commands?
|
||||
# If False, the LLM only replies to the "ask:" and "askai" commands.
|
||||
llmReplyToNonCommands = True
|
||||
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
StoreLimit = 3
|
||||
|
||||
# history command
|
||||
enableCmdHistory = True
|
||||
# command history ignore list ex: 2813308004,4258675309
|
||||
lheardCmdIgnoreNodes =
|
||||
|
||||
# 24 hour clock
|
||||
zuluTime = False
|
||||
# wait time for URL requests
|
||||
urlTimeout = 10
|
||||
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = True
|
||||
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = False
|
||||
# channel to send a message to when the emergency handler is triggered
|
||||
alert_channel = 2
|
||||
alert_interface = 1
|
||||
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
emailSentryAlerts = False
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryChannel = 9
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
# Altitude in meters to trigger the alert
|
||||
highFlyingAlertAltitude = 2000
|
||||
# Channel to send Alert when the high flying node is detected
|
||||
highFlyingAlertChannel = 2
|
||||
|
||||
[bbs]
|
||||
enabled = True
|
||||
@@ -93,26 +119,89 @@ enabled = True
|
||||
bbs_ban_list =
|
||||
# list of admin nodes numbers ex: 2813308004,4258675309
|
||||
bbs_admin_list =
|
||||
# enable bbs synchronization with other nodes
|
||||
bbslink_enabled = False
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
|
||||
# location module
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
# EAS Alert Broadcast
|
||||
|
||||
# NOAA weather forecast days
|
||||
NOAAforecastDuration = 3
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
|
||||
# NOAA Hydrology unique identifiers, LID or USGS ID
|
||||
riverListDefault =
|
||||
|
||||
# NOAA EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreEASenable = False
|
||||
ignoreEASwords = test,advisory
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
# Add extra location to the weather alert
|
||||
enableExtraLocationWx = False
|
||||
|
||||
# Goverment Alert Broadcast defaults to FEMA IPAWS
|
||||
eAlertBroadcastEnabled = False
|
||||
# Goverment Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
|
||||
# FEMA Alert Broadcast Settings
|
||||
# Enable Ignore any headline that includes following word list
|
||||
ignoreFEMAenable = True
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreUSGSEnable = False
|
||||
ignoreUSGSWords = test,advisory
|
||||
|
||||
# Use DE Alert Broadcast Data
|
||||
enableDEalerts = False
|
||||
# comma separated list of regional codes trigger local alert.
|
||||
# find your regional codet at https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
|
||||
# CheckList Checkin/Checkout
|
||||
[checklist]
|
||||
enabled = False
|
||||
checklist_db = data/checklist.db
|
||||
reverse_in_out = False
|
||||
|
||||
[qrz]
|
||||
# QRZ Hello to new nodes with message
|
||||
enabled = False
|
||||
qrz_db = data/qrz.db
|
||||
qrz_hello_string = "MeshBot says Hello! DM for more info."
|
||||
# Training mode will not send the hello message to new nodes
|
||||
training = True
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -121,7 +210,22 @@ enabled = False
|
||||
# and rebroadcasted on the same channel on the other device/node/interface
|
||||
# with great power comes great responsibility, danger could be lurking in use of this feature
|
||||
# if you have the two nodes on the same radio configurations, you could create a feedback loop
|
||||
repeater_channels =
|
||||
repeater_channels =
|
||||
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
|
||||
[radioMon]
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
@@ -138,15 +242,67 @@ signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
|
||||
[fileMon]
|
||||
enabled = False
|
||||
filemon_enabled = False
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
# only return a single random line from the news file
|
||||
news_random_line = False
|
||||
# enable the use of exernal shell commands
|
||||
enable_runShellCmd = False
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
hangman = True
|
||||
hamtest = True
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 0.7
|
||||
responseDelay = 1.2
|
||||
# delay in seconds for splits in messages to avoid message collision
|
||||
splitDelay = 0.0
|
||||
# message chunk size for sending at high success rate
|
||||
# message chunk size for sending at high success rate, chunkr allows exceeding by 3 characters
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max limit buffer for radio testing
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
|
||||
|
||||
|
||||
7226
data/hamradio/extra.json
Normal file
7226
data/hamradio/extra.json
Normal file
File diff suppressed because it is too large
Load Diff
5126
data/hamradio/general.json
Normal file
5126
data/hamradio/general.json
Normal file
File diff suppressed because it is too large
Load Diff
4934
data/hamradio/technician.json
Normal file
4934
data/hamradio/technician.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Substitute environment variables in the config file
|
||||
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
|
||||
exec python /app/mesh_bot.py
|
||||
@@ -23,6 +23,27 @@ except Exception as e:
|
||||
except Exception as e:
|
||||
bbs_dm = "System: data/bbsdm.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
email_db = "System: data/email_db.pickle not found"
|
||||
|
||||
try:
|
||||
with open('../data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
sms_db = "System: data/sms_db.pickle not found"
|
||||
|
||||
|
||||
# Game HS tables
|
||||
try:
|
||||
with open('../data/lemonstand.pkl', 'rb') as f:
|
||||
@@ -90,6 +111,10 @@ print ("System: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
print ("\nSystem: email_db")
|
||||
print (email_db)
|
||||
print ("\nSystem: sms_db")
|
||||
print (sms_db)
|
||||
print (f"\n\nGame HS tables\n")
|
||||
print (f"lemon:{lemon_score}")
|
||||
print (f"dopewar:{dopewar_score}")
|
||||
|
||||
@@ -9,32 +9,41 @@ from EAS2Text import EAS2Text
|
||||
buff=[] # store messages for writing
|
||||
seen=set()
|
||||
pattern = re.compile(r'ZCZC.*?NWS-')
|
||||
|
||||
# alternate regex for parsing multimon-ng output
|
||||
# provided by https://github.com/A-c0rN
|
||||
#reg = r"^.*?(NNNN|ZCZC)(?:-([A-Za-z0-9]{3})-([A-Za-z0-9]{3})-((?:-?[0-9]{6})+)\+([0-9]{4})-([0-9]{7})-(.{8})-)?.*?$"
|
||||
#prog = re.compile(reg, re.MULTILINE)
|
||||
#groups = prog.match(sameData).groups()
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Handle piped input
|
||||
line=input().strip()
|
||||
inp=input().strip()
|
||||
except EOFError:
|
||||
break
|
||||
# only want EAS lines
|
||||
if line.startswith("EAS:") or line.startswith("EAS (part):"):
|
||||
content=line.split(maxsplit=1)[1]
|
||||
if content=="NNNN": # end of EAS message
|
||||
# write if we have something
|
||||
if buff:
|
||||
print("writing")
|
||||
with open("alert.txt","w") as fh:
|
||||
fh.write('\n'.join(buff))
|
||||
# prepare for new data
|
||||
buff.clear()
|
||||
seen.clear()
|
||||
elif content in seen:
|
||||
# don't need repeats'
|
||||
continue
|
||||
else:
|
||||
# check for national weather service
|
||||
match=pattern.search(content)
|
||||
if match:
|
||||
seen.add(content)
|
||||
msg=EAS2Text(content).EASText
|
||||
print("got message", msg)
|
||||
buff.append(msg)
|
||||
# potentially take multiple lines in one buffered input
|
||||
for line in inp.splitlines():
|
||||
# only want EAS lines
|
||||
if line.startswith("EAS:") or line.startswith("EAS (part):"):
|
||||
content=line.split(":", maxsplit=1)[1].strip()
|
||||
if content=="NNNN": # end of EAS message
|
||||
# write if we have something
|
||||
if buff:
|
||||
print("writing")
|
||||
with open("alert.txt","w") as fh:
|
||||
fh.write('\n'.join(buff))
|
||||
# prepare for new data
|
||||
buff.clear()
|
||||
seen.clear()
|
||||
elif content in seen:
|
||||
# don't need repeats
|
||||
continue
|
||||
else:
|
||||
# check for national weather service
|
||||
match=pattern.search(content)
|
||||
if match:
|
||||
seen.add(content)
|
||||
msg=EAS2Text(content).EASText
|
||||
print("got message", msg)
|
||||
buff.append(msg)
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot.service
|
||||
# sudo systemctl start mesh_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl start mesh_bot.service
|
||||
# sudo systemctl enable mesh_bot_reporting.service
|
||||
# sudo systemctl start mesh_bot_reporting.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-Reporting
|
||||
|
||||
23
etc/mesh_bot_w3.tmp
Normal file
23
etc/mesh_bot_w3.tmp
Normal file
@@ -0,0 +1,23 @@
|
||||
# /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot_w3.service
|
||||
# sudo systemctl start mesh_bot_w3.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-W3Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable pong_bot.service
|
||||
# sudo systemctl start pong_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
@@ -57,7 +57,7 @@ def parse_log_file(file_path):
|
||||
if multiLogReader:
|
||||
# set file_path to the cwd of the default project ../log
|
||||
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
|
||||
print(f"Checking log files: {log_files}")
|
||||
|
||||
if log_files:
|
||||
@@ -350,7 +350,8 @@ def get_database_info():
|
||||
os.path.join(base_dir, 'mmind_hs.pkl'),
|
||||
os.path.join(base_dir, 'golfsim_hs.pkl'),
|
||||
os.path.join(base_dir, 'bbsdb.pkl'),
|
||||
os.path.join(base_dir, 'bbsdm.pkl')]
|
||||
os.path.join(base_dir, 'bbsdm.pkl'),
|
||||
os.path.join(base_dir, 'qrz.db')]
|
||||
|
||||
for file in databaseFiles:
|
||||
try:
|
||||
@@ -371,6 +372,16 @@ def get_database_info():
|
||||
bbsdb = pickle.load(f)
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
elif 'qrz.db' in file:
|
||||
# open the qrz.db sqllite file
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM qrz")
|
||||
qrz_db = cursor.fetchall()
|
||||
# convert to a list of strings
|
||||
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
@@ -425,7 +436,8 @@ def get_database_info():
|
||||
'golfsim_score': golfsim_score,
|
||||
'banList': banList,
|
||||
'adminList': adminList,
|
||||
'sentryIgnoreList': sentryIgnoreList
|
||||
'sentryIgnoreList': sentryIgnoreList,
|
||||
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
|
||||
}
|
||||
|
||||
def generate_main_html(log_data, system_info):
|
||||
@@ -745,7 +757,7 @@ def generate_main_html(log_data, system_info):
|
||||
"""
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(
|
||||
date=datetime.now().strftime('%Y_%m_%d'),
|
||||
date=datetime.now().strftime('%Y-%m-%d'),
|
||||
command_data=json.dumps(log_data['command_counts']),
|
||||
message_data=json.dumps(log_data['message_types']),
|
||||
activity_data=json.dumps(log_data['hourly_activity']),
|
||||
@@ -913,6 +925,11 @@ def generate_database_html(database_info):
|
||||
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
|
||||
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
|
||||
</table>
|
||||
<h1>QRZ Database</h1>
|
||||
<p>QRZ Database holds heard nodeID and Shortname</p>
|
||||
<table>
|
||||
<tr><td>${qrz_db}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@@ -58,7 +58,7 @@ def parse_log_file(file_path):
|
||||
if multiLogReader:
|
||||
# set file_path to the cwd of the default project ../log
|
||||
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
|
||||
print(f"Checking log files: {log_files}")
|
||||
|
||||
if log_files:
|
||||
@@ -359,7 +359,8 @@ def get_database_info():
|
||||
os.path.join(base_dir, 'mmind_hs.pkl'),
|
||||
os.path.join(base_dir, 'golfsim_hs.pkl'),
|
||||
os.path.join(base_dir, 'bbsdb.pkl'),
|
||||
os.path.join(base_dir, 'bbsdm.pkl')]
|
||||
os.path.join(base_dir, 'bbsdm.pkl'),
|
||||
os.path.join(base_dir, 'qrz.db')]
|
||||
|
||||
for file in databaseFiles:
|
||||
try:
|
||||
@@ -380,6 +381,16 @@ def get_database_info():
|
||||
bbsdb = pickle.load(f)
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
elif 'qrz.db' in file:
|
||||
#open the qrz.db sqllite file
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM qrz")
|
||||
qrz_db = cursor.fetchall()
|
||||
# convert to a list of strings
|
||||
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
@@ -434,7 +445,8 @@ def get_database_info():
|
||||
'golfsim_score': golfsim_score,
|
||||
'banList': banList,
|
||||
'adminList': adminList,
|
||||
'sentryIgnoreList': sentryIgnoreList
|
||||
'sentryIgnoreList': sentryIgnoreList,
|
||||
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
|
||||
}
|
||||
|
||||
def generate_main_html(log_data, system_info):
|
||||
@@ -1036,7 +1048,7 @@ options: {
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(
|
||||
date=datetime.now().strftime('%Y_%m_%d'),
|
||||
date=datetime.now().strftime('%Y-%m-%d'),
|
||||
command_data=json.dumps(log_data['command_counts']),
|
||||
message_data=json.dumps(log_data['message_types']),
|
||||
activity_data=json.dumps(log_data['hourly_activity']),
|
||||
@@ -1207,6 +1219,11 @@ def generate_database_html(database_info):
|
||||
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
|
||||
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
|
||||
</table>
|
||||
<h1>QRZ Database</h1>
|
||||
<p>QRZ Database holds heard nodeID and Shortname</p>
|
||||
<table>
|
||||
<tr><td>${qrz_db}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# # Simulate meshing-around de K7MHI 2024
|
||||
from modules.log import * # Import the logger
|
||||
from modules.log import * # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
395
install.sh
395
install.sh
@@ -1,159 +1,360 @@
|
||||
#!/bin/bash
|
||||
# meshing-around install helper script
|
||||
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
|
||||
printf "\nChecking for dependencies\n"
|
||||
printf "########################\n"
|
||||
printf "\nThis script will try and install the Meshing Around Bot and its dependencies.\n"
|
||||
printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded systems.\n"
|
||||
printf "If there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# check if we are in /opt/meshing-around
|
||||
if [ $program_path != "/opt/meshing-around" ]; then
|
||||
printf "\nIt is suggested to project path to /opt/meshing-around\n"
|
||||
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
|
||||
read move
|
||||
if [[ $(echo "$move" | grep -i "^y") ]]; then
|
||||
sudo mv $program_path /opt/meshing-around
|
||||
cd /opt/meshing-around
|
||||
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# check write access to program path
|
||||
if [[ ! -w ${program_path} ]]; then
|
||||
printf "\nInstall path not writable, try running the installer with sudo\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if hostname = femtofox, then we are on embedded
|
||||
if [[ $(hostname) == "femtofox" ]]; then
|
||||
printf "\nDetected femtofox embedded system\n"
|
||||
embedded="y"
|
||||
else
|
||||
# check if running on embedded
|
||||
printf "\nAre You installing into an embedded system like a luckfox or -native? most should say no here (y/n)"
|
||||
read embedded
|
||||
fi
|
||||
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
printf "\nDetected embedded skipping dependency installation\n"
|
||||
else
|
||||
# Check and install dependencies
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
|
||||
sudo apt-get install python3 python3-pip
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, trying 'apt-get install python3-pip'\n"
|
||||
sudo apt-get install python3-pip
|
||||
fi
|
||||
|
||||
# double check for python3 and pip
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, please install python3 with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\nDependencies installed\n"
|
||||
fi
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout and tty groups for serial access\n"
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout $USER
|
||||
sudo usermod -a -G tty $USER
|
||||
sudo usermod -a -G bluetooth $USER
|
||||
|
||||
# check for pip
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
sudo apt-get install python3-pip
|
||||
else
|
||||
printf "python pip found\n"
|
||||
fi
|
||||
# copy service files
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [ -f config.ini ]; then
|
||||
if [[ -f config.ini ]]; then
|
||||
printf "\nConfig file already exists, moving to backup config.old\n"
|
||||
mv config.ini config.old
|
||||
fi
|
||||
|
||||
cp config.template config.ini
|
||||
printf "\nConfig file generated\n"
|
||||
printf "\nConfig files generated!\n"
|
||||
|
||||
# update lat,long in config.ini
|
||||
latlong=$(curl --silent --max-time 20 https://ipinfo.io/loc || echo "48.50,-123.0")
|
||||
IFS=',' read -r lat lon <<< "$latlong"
|
||||
sed -i "s|lat = 48.50|lat = $lat|g" config.ini
|
||||
sed -i "s|lon = -123.0|lon = $lon|g" config.ini
|
||||
echo "lat,long updated in config.ini to $latlong"
|
||||
|
||||
# set virtual environment and install dependencies
|
||||
printf "\nMeshing Around Installer\n"
|
||||
|
||||
echo "Do you want to install the bot in a virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFpund virtual environment for python\n"
|
||||
else
|
||||
sudo apt-get install python3-venv
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
# check if running on embedded
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
printf "\nDetected embedded skipping venv\n"
|
||||
else
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [ $rpi == "y" ]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
printf "\nRecomended install is in a python virtual environment, do you want to use venv? (y/n)"
|
||||
read venv
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3/venv error, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "The Following could be messy, or take some time on slower devices."
|
||||
echo "Creating virtual environment..."
|
||||
#check if python3 has venv module
|
||||
if [[ -f venv/bin/activate ]]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
|
||||
sudo apt-get install python3-venv
|
||||
fi
|
||||
# create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# double check for python3-venv
|
||||
if [[ -f venv/bin/activate ]]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\nVirtual environment created\n"
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies to venv
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies to system
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [[ $(echo "${rpi}" | grep -i "^y") ]]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n\n"
|
||||
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
|
||||
read bot
|
||||
# if $1 is passed
|
||||
if [[ $1 == "pong" ]]; then
|
||||
bot="pong"
|
||||
elif [[ $1 == "mesh" ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
bot="mesh"
|
||||
else
|
||||
printf "\n\n"
|
||||
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
|
||||
echo "Pong bot is a simple bot for network testing"
|
||||
echo "Mesh bot is a more complex bot more suited for meshing around"
|
||||
echo "None will skip the service install"
|
||||
read bot
|
||||
fi
|
||||
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
# set the correct user in the service file?
|
||||
whoami=$(whoami)
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "\nDo you want to add a local user (meshbot) no login, for the bot? (y/n)"
|
||||
read meshbotservice
|
||||
fi
|
||||
|
||||
if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
sudo useradd -M meshbot
|
||||
sudo usermod -L meshbot
|
||||
sudo groupadd meshbot
|
||||
sudo usermod -a -G meshbot meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
else
|
||||
whoami=$(whoami)
|
||||
fi
|
||||
# set basic permissions for the bot user
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added user $whoami to dialout, tty, and bluetooth groups"
|
||||
|
||||
sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sudo systemctl daemon-reload
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable pong_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start pong bot service: systemctl start pong_bot"
|
||||
service="pong_bot"
|
||||
fi
|
||||
|
||||
if [ $bot == "mesh" ]; then
|
||||
if [[ $(echo "${bot}" | grep -i "^m") ]]; then
|
||||
# install service for mesh bot
|
||||
sudo cp etc/mesh_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable mesh_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start mesh bot service: systemctl start mesh_bot"
|
||||
service="mesh_bot"
|
||||
fi
|
||||
|
||||
if [ $bot == "n" ]; then
|
||||
if [ -f launch.sh ]; then
|
||||
printf "\nTo run the bot, use the command: ./launch.sh\n"
|
||||
./launch.sh
|
||||
# check if running on embedded for final steps
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
# ask if emoji font should be installed for linux
|
||||
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [[ $(echo "${emoji}" | grep -i "^y") ]]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "ollama pull gemma2:2b\n"
|
||||
printf "Total download is multi GB, recomend pi5/8GB or better for this\n"
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [[ $(echo "${ollama}" | grep -i "^y") ]]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
echo "Do you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [ $gemma == "y" ]; then
|
||||
ollama pull gemma2:2b
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
|
||||
ollama pull gemma2:2b
|
||||
fi
|
||||
fi
|
||||
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
read -p "Press enter to complete the installation, these commands saved to install_notes.txt"
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [[ $(echo "${reboot}" | grep -i "^y") ]]; then
|
||||
sudo reboot
|
||||
fi
|
||||
else
|
||||
# we are on embedded
|
||||
# replace "type = serial" with "type = tcp" in config.ini
|
||||
replace="s|type = serial|type = tcp|g"
|
||||
sed -i "$replace" config.ini
|
||||
# replace "# hostname = meshtastic.local" with "hostname = localhost" in config.ini
|
||||
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nConfig file updated for embedded\n"
|
||||
# add service dependency for meshtasticd into service file
|
||||
#replace="s|After=network.target|After=network.target meshtasticd.service|g"
|
||||
|
||||
# Set up the meshing around service
|
||||
sudo cp /opt/meshing-around/etc/$service.service /etc/systemd/system/$service.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
# check if the cron job already exists
|
||||
if ! crontab -l | grep -q "$chronjob"; then
|
||||
# add the cron job to run the report_generator5.py script
|
||||
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
|
||||
printf "\nAdded cron job to run report_generator5.py\n"
|
||||
else
|
||||
printf "\nCron job already exists, skipping\n"
|
||||
fi
|
||||
printf "Reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
fi
|
||||
|
||||
echo "Good time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
printf "\nInstallation complete!\n"
|
||||
|
||||
exit 0
|
||||
|
||||
# to uninstall the product run the following commands as needed
|
||||
|
||||
# sudo systemctl stop mesh_bot
|
||||
# sudo systemctl disable mesh_bot
|
||||
# sudo systemctl stop pong_bot
|
||||
# sudo systemctl disable pong_bot
|
||||
# sudo systemctl stop mesh_bot_reporting
|
||||
# sudo systemctl disable mesh_bot_reporting
|
||||
# sudo rm /etc/systemd/system/mesh_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo rm /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl reset-failed
|
||||
|
||||
# sudo gpasswd -d meshbot dialout
|
||||
# sudo gpasswd -d meshbot tty
|
||||
# sudo gpasswd -d meshbot bluetooth
|
||||
# sudo groupdel meshbot
|
||||
# sudo userdel meshbot
|
||||
|
||||
# sudo rm -rf /opt/meshing-around
|
||||
|
||||
|
||||
# after install shenannigans
|
||||
# add 'bee = True' to config.ini General section. You will likley want to clean the txt up a bit
|
||||
# wget https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt -O bee.txt
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# This script launches the meshing-around bot or the report generator in python virtual environment
|
||||
|
||||
# launch.sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
# Logs and Reports
|
||||
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
|
||||
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed.
|
||||
- `multi_log_reader = True` on by default will read all logs (or set to false to return daily logs)
|
||||
## Reporting Note
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
|
||||
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
|
||||
- provided serviceTimer templates in etc/
|
||||
- If you are in a venv and using launch.sh you can `launch.sh html5`
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
|
||||
```
|
||||
```conf
|
||||
[general]
|
||||
# logging to file of the non Bot messages only
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file, needed for reporting engine
|
||||
SyslogToFile = True
|
||||
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
```
|
||||
## Web Reporting WebServer
|
||||
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
|
||||
|
||||
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
|
||||
find it at. http://localhost:8420
|
||||
|
||||
If you have linux-native running and errors such as..
|
||||
```bash
|
||||
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
|
||||
self.socket.bind(self.server_address)
|
||||
```
|
||||
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
|
||||
|
||||
```python
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
```
|
||||
# Set level for stdout handler
|
||||
stdout_handler.setLevel(logging.INFO)
|
||||
```
|
||||
703
mesh_bot.py
703
mesh_bot.py
File diff suppressed because it is too large
Load Diff
54
modules/README.md
Normal file
54
modules/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Modules and Adding stuff
|
||||
|
||||
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
|
||||
|
||||
## By following these steps, you can add a new bbs option to the bot.
|
||||
|
||||
1. **Define the Command Handler**:
|
||||
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
|
||||
```python
|
||||
def handle_newcommand(message, message_from_id, deviceID):
|
||||
return "This is a response from the new command."
|
||||
```
|
||||
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
|
||||
2. **Add the Command to the Auto Response**:
|
||||
Update the auto_response function in mesh_bot.py to include the new command:
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
|
||||
#...
|
||||
```
|
||||
3. **Update the Trap List and Help**:
|
||||
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
|
||||
```python
|
||||
#...
|
||||
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
|
||||
help_message = "Bot CMD?:newcommand, "
|
||||
#...
|
||||
```
|
||||
|
||||
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
|
||||
```python
|
||||
# newcommand Configuration
|
||||
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
|
||||
if newcommand_enabled:
|
||||
trap_list_newcommand = ("newcommand",)
|
||||
trap_list = trap_list + trap_list_newcommand
|
||||
help_message = help_message + ", newcommand"
|
||||
```
|
||||
|
||||
5. **Test the New Command**:
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
|
||||
|
||||
### Running a Shell command
|
||||
|
||||
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
|
||||
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"switchON": lambda: call_external_script(message)
|
||||
```
|
||||
This would call the default script located in script/runShell.sh and return its output.
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
|
||||
|
||||
@@ -164,6 +165,15 @@ def bbs_delete_dm(toNode, message):
|
||||
|
||||
def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
# check if the bbs link is enabled
|
||||
if bbs_link_whitelist != ['']:
|
||||
if str(peerNode) not in bbs_link_whitelist:
|
||||
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
|
||||
return "System: BBS Link is disabled for your node."
|
||||
if bbs_link_enabled == False:
|
||||
return "System: BBS Link is disabled."
|
||||
|
||||
# respond when another bot asks for the bbs posts to sync
|
||||
if "bbslink" in input.lower():
|
||||
if "$" in input and "#" in input:
|
||||
@@ -175,11 +185,21 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
return f"bbsack {messageID}"
|
||||
elif "bbsack" in input.lower():
|
||||
# increment the messageID
|
||||
ack = int(input.split(" ")[1])
|
||||
messageID = int(ack) + 1
|
||||
if len(input.split(" ")) > 1:
|
||||
try:
|
||||
messageID = int(input.split(" ")[1]) + 1
|
||||
except:
|
||||
return "link error"
|
||||
else:
|
||||
return "link error"
|
||||
|
||||
# send message
|
||||
# send message with delay to keep chutil happy
|
||||
if messageID < len(bbs_messages):
|
||||
logger.debug(f"System: Sending bbslink message {messageID} to peer " + str(peerNode))
|
||||
time.sleep(5 + responseDelay)
|
||||
# every 5 messages add extra delay
|
||||
if messageID % 5 == 0:
|
||||
time.sleep(10 + responseDelay)
|
||||
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
|
||||
else:
|
||||
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
|
||||
|
||||
160
modules/checklist.py
Normal file
160
modules/checklist.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Checkin Checkout database module for the bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
import time
|
||||
|
||||
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout")
|
||||
|
||||
def initialize_checklist_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
# Check if the checkin table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkin
|
||||
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
|
||||
# Check if the checkout table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkout
|
||||
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.debug("System: Ensured data/checklist.db exists with required tables")
|
||||
|
||||
def checkin(name, date, time, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
# checkin a user
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
# # remove any checkouts that are older than the checkin
|
||||
# c.execute("DELETE FROM checkout WHERE checkout_date < ? OR (checkout_date = ? AND checkout_time < ?)", (date, date, time))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if reverse_in_out:
|
||||
return "Checked✅Out: " + str(name)
|
||||
else:
|
||||
return "Checked✅In: " + str(name)
|
||||
|
||||
def delete_checkin(checkin_id):
|
||||
# delete a checkin
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM checkin WHERE checkin_id = ?", (checkin_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checkin deleted." + str(checkin_id)
|
||||
|
||||
def checkout(name, date, time_str, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
# checkout a user
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
# Check if the user has a checkin before checking out
|
||||
c.execute("""
|
||||
SELECT checkin_id FROM checkin
|
||||
WHERE checkin_name = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM checkout
|
||||
WHERE checkout_name = checkin_name
|
||||
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
|
||||
)
|
||||
ORDER BY checkin_date DESC, checkin_time DESC
|
||||
LIMIT 1
|
||||
""", (name,))
|
||||
checkin_record = c.fetchone()
|
||||
if checkin_record:
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
# calculate length of time checked in
|
||||
c.execute("SELECT checkin_time FROM checkin WHERE checkin_id = ?", (checkin_record[0],))
|
||||
checkin_time = c.fetchone()[0]
|
||||
checkin_datetime = time.strptime(date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
|
||||
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds))
|
||||
# # remove the checkin record older than the checkout
|
||||
# c.execute("DELETE FROM checkin WHERE checkin_date < ? OR (checkin_date = ? AND checkin_time < ?)", (date, date, time_str))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if checkin_record:
|
||||
if reverse_in_out:
|
||||
return "Checked⌛️In: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "Checked⌛️Out: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "None found for " + str(name)
|
||||
|
||||
def delete_checkout(checkout_id):
|
||||
# delete a checkout
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM checkout WHERE checkout_id = ?", (checkout_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checkout deleted." + str(checkout_id)
|
||||
|
||||
def list_checkin():
|
||||
# list checkins
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("""
|
||||
SELECT * FROM checkin
|
||||
WHERE checkin_id NOT IN (
|
||||
SELECT checkin_id FROM checkout
|
||||
WHERE checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time)
|
||||
)
|
||||
""")
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
timeCheckedIn = ""
|
||||
checkin_list = ""
|
||||
for row in rows:
|
||||
#calculate length of time checked in
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))))
|
||||
checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn
|
||||
if row[5] != "":
|
||||
checkin_list += "📝" + row[5]
|
||||
if row != rows[-1]:
|
||||
checkin_list += "\n"
|
||||
# if empty list
|
||||
if checkin_list == "":
|
||||
return "No data to display."
|
||||
return checkin_list
|
||||
|
||||
def process_checklist_command(nodeID, message, name="none", location="none"):
|
||||
current_date = time.strftime("%Y-%m-%d")
|
||||
current_time = time.strftime("%H:%M:%S")
|
||||
# if user on bbs_ban_list reject command
|
||||
if str(nodeID) in bbs_ban_list:
|
||||
logger.warning("System: Checklist attempt from the ban list")
|
||||
return "unable to process command"
|
||||
try:
|
||||
comment = message.split(" ", 1)[1]
|
||||
except IndexError:
|
||||
comment = ""
|
||||
# handle checklist commands
|
||||
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
|
||||
return checkin(name, current_date, current_time, location, comment)
|
||||
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
|
||||
return checkout(name, current_date, current_time, location, comment)
|
||||
elif "purgein" in message.lower():
|
||||
return delete_checkin(nodeID)
|
||||
elif "purgeout" in message.lower():
|
||||
return delete_checkout(nodeID)
|
||||
elif "checklist" in message.lower():
|
||||
return list_checkin()
|
||||
else:
|
||||
return "Invalid command."
|
||||
@@ -3,17 +3,49 @@
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import random
|
||||
import os
|
||||
|
||||
async def watch_file():
|
||||
def read_file(file_monitor_file_path):
|
||||
try:
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
def read_file(file_monitor_file_path, random_line_only=False):
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
logger.warning(f"FileMon: File not found: {file_monitor_file_path}")
|
||||
if file_monitor_file_path == "bee.txt":
|
||||
return "🐝buzz 💐buzz buzz🍯"
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
return random.choice(lines)
|
||||
else:
|
||||
# read the whole file
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path, news_random_line_only)
|
||||
|
||||
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
try:
|
||||
with open(news_file_path, 'a' if append else 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
logger.info(f"FileMon: Updated {news_file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error writing file: {news_file_path}")
|
||||
return False
|
||||
|
||||
async def watch_file():
|
||||
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
return None
|
||||
@@ -29,4 +61,24 @@ async def watch_file():
|
||||
content = content.replace('\n', ' ').replace('\r', '').strip()
|
||||
if content:
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
try:
|
||||
# Debugging: Print the current working directory and resolved script path
|
||||
current_working_directory = os.getcwd()
|
||||
script_path = os.path.join(current_working_directory, script)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
# try the raw script name
|
||||
script_path = script
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
return None
|
||||
|
||||
@@ -159,7 +159,7 @@ def get_found_items(nodeID):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
dwInventoryDb[i]['amount'] = amount
|
||||
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
|
||||
msg = f"💊You found {qty} {my_drugs[found].name}"
|
||||
else:
|
||||
# rolls to see how much cash the user finds
|
||||
cash_found = random.randint(1, 977)
|
||||
@@ -241,6 +241,8 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
|
||||
buy_amount = cash // price_list[drug_choice]
|
||||
if buy_amount > 100 - inventory:
|
||||
buy_amount = 100 - inventory
|
||||
if buy_amount == 0:
|
||||
return "You don\'t have any empty inventory slots.🎒"
|
||||
# set the buy amount to the max if the user enters m
|
||||
buy_amount = int(buy_amount)
|
||||
|
||||
|
||||
142
modules/games/hamtest.py
Normal file
142
modules/games/hamtest.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# hamradio test module for meshbot DE K7MHI 2025
|
||||
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
# data files which are expected to be in ../../data/hamradio/ similar to the following:
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
|
||||
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from modules.log import *
|
||||
|
||||
class HamTest:
|
||||
def __init__(self):
|
||||
self.questions = {}
|
||||
self.load_questions()
|
||||
self.game = {}
|
||||
|
||||
def load_questions(self):
|
||||
for level in ['technician', 'general', 'extra']:
|
||||
try:
|
||||
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
|
||||
self.questions[level] = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
|
||||
def newGame(self, id, level='technician'):
|
||||
msg = f"📻New {level} quiz started, 'end' to exit."
|
||||
if id in self.game:
|
||||
level = self.game[id]['level']
|
||||
self.game[id] = {
|
||||
'level': level,
|
||||
'score': 0,
|
||||
'total': 0,
|
||||
'errors': [],
|
||||
'qId': None,
|
||||
'question': None,
|
||||
'answers': None,
|
||||
'correct': None
|
||||
}
|
||||
# set the pool needed for the game
|
||||
if self.game[id]['level'] == 'extra':
|
||||
self.game[id]['total'] = 50
|
||||
else:
|
||||
self.game[id]['total'] = 35
|
||||
|
||||
# randomize the questions
|
||||
random.shuffle(self.questions[level])
|
||||
|
||||
msg += f"\n{self.nextQuestion(id)}"
|
||||
return msg
|
||||
|
||||
def nextQuestion(self, id):
|
||||
level = self.game[id]['level']
|
||||
# if question has the word figure in it, skip it
|
||||
question = random.choice(self.questions[level])
|
||||
while 'figure' in question['question'].lower():
|
||||
question = random.choice(self.questions[level])
|
||||
|
||||
self.game[id]['question'] = question['question']
|
||||
self.game[id]['answers'] = question['answers']
|
||||
self.game[id]['correct'] = question['correct']
|
||||
self.game[id]['qId'] = question['id']
|
||||
self.game[id]['total'] -= 1
|
||||
|
||||
if self.game[id]['total'] == 0:
|
||||
return self.endGame(id)
|
||||
|
||||
# ask the question and return answers in A, B, C, D format
|
||||
msg = f"{self.game[id]['question']}\n"
|
||||
for i, answer in enumerate(self.game[id]['answers']):
|
||||
msg += f"{chr(65+i)}. {answer}\n"
|
||||
return msg
|
||||
|
||||
def answer(self, id, answer):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
if self.game[id]['correct'] == ord(answer.upper()) - 65:
|
||||
self.game[id]['score'] += 1
|
||||
return f"Correct👍\n" + self.nextQuestion(id)
|
||||
else:
|
||||
# record the section of the question for study aid
|
||||
section = self.game[id]['qId'][:3]
|
||||
self.game[id]['errors'].append(section)
|
||||
# provide the correct answer
|
||||
answer = [self.game[id]['correct']]
|
||||
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
|
||||
|
||||
def getScore(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
score = self.game[id]['score']
|
||||
total = self.game[id]['total']
|
||||
level = self.game[id]['level']
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
|
||||
if level == 'extra':
|
||||
pool = 50
|
||||
else:
|
||||
pool = 35
|
||||
|
||||
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
|
||||
|
||||
def endGame(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
|
||||
score = self.game[id]['score']
|
||||
level = self.game[id]['level']
|
||||
|
||||
if level == 'extra':
|
||||
# passing score for extra is 37 out of 50
|
||||
passing = 37
|
||||
else:
|
||||
# passing score for technician and general is 26 out of 35
|
||||
passing = 26
|
||||
|
||||
if score >= passing:
|
||||
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
|
||||
else:
|
||||
# find the most common section of the questions missed
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
|
||||
|
||||
# remove the game[id] from the list
|
||||
del self.game[id]
|
||||
return msg
|
||||
|
||||
hamtestTracker = []
|
||||
hamtest = HamTest()
|
||||
|
||||
203
modules/games/hangman.py
Normal file
203
modules/games/hangman.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
|
||||
import random
|
||||
|
||||
class Hangman:
|
||||
WORDS = [
|
||||
"ability","able","about","above","accept","according","account","across",
|
||||
"act","action","activity","actually","add","address","administration","admit",
|
||||
"adult","affect","after","again","against","age","agency","agent","ago",
|
||||
"agree","agreement","ahead","air","all","allow","almost","alone","along",
|
||||
"already","also","although","always","American","among","amount","analysis",
|
||||
"and","animal","another","answer","any","anyone","anything","appear","apply",
|
||||
"approach","area","argue","arm","around","arrive","art","article","artist",
|
||||
"as","ask","assume","at","attack","attention","attorney","audience","author",
|
||||
"authority","available","avoid","away","baby","back","bad","bag","ball",
|
||||
"bank","bar","base","be","beat","beautiful","because","become","bed","before",
|
||||
"begin","behavior","behind","believe","benefit","best","better","between",
|
||||
"beyond","big","bill","billion","bit","black","blood","blue","board","body",
|
||||
"book","born","both","box","boy","break","bring","brother","budget","build",
|
||||
"building","business","but","buy","by","call","camera","campaign","can",
|
||||
"cancer","candidate","capital","car","card","care","career","carry","case",
|
||||
"catch","cause","cell","center","central","century","certain","certainly",
|
||||
"chair","challenge","chance","change","character","charge","check","child",
|
||||
"choice","choose","church","citizen","city","civil","claim","class","clear",
|
||||
"clearly","close","coach","cold","collection","college","color","come",
|
||||
"commercial","common","community","company","compare","computer","concern",
|
||||
"condition","conference","Congress","consider","consumer","contain","continue",
|
||||
"control","cost","could","country","couple","course","court","cover","create",
|
||||
"crime","cultural","culture","cup","current","customer","cut","dark","data",
|
||||
"daughter","day","dead","deal","death","debate","decade","decide","decision",
|
||||
"deep","defense","degree","democrat","democratic","describe","design",
|
||||
"despite","detail","determine","develop","development","die","difference",
|
||||
"different","difficult","dinner","direction","director","discover","discuss",
|
||||
"discussion","disease","do","doctor","dog","door","down","draw","dream","drive",
|
||||
"drop","drug","during","each","early","east","easy","eat","economic","economy",
|
||||
"edge","education","effect","effort","eight","either","election","else",
|
||||
"employee","end","energy","enjoy","enough","enter","entire","environment",
|
||||
"environmental","especially","establish","even","evening","event","ever",
|
||||
"every","everybody","everyone","everything","evidence","exactly","example",
|
||||
"executive","exist","expect","experience","expert","explain","eye","face",
|
||||
"fact","factor","fail","fall","family","far","fast","father","fear","federal",
|
||||
"feel","feeling","few","field","fight","figure","fill","film","final","finally",
|
||||
"financial","find","fine","finger","finish","fire","firm","first","fish","five",
|
||||
"floor","fly","focus","follow","food","foot","for","force","foreign","forget",
|
||||
"form","former","forward","four","free","friend","from","front","full","fund",
|
||||
"future","game","garden","gas","general","generation","get","girl","give",
|
||||
"glass","go","goal","good","government","great","green","ground","group","grow",
|
||||
"growth","guess","gun","guy","hair","half","hand","hang","happen","happy",
|
||||
"hard","have","he","head","health","hear","heart","heat","heavy","help","her",
|
||||
"here","herself","high","him","himself","his","history","hit","hold","home",
|
||||
"hope","hospital","hot","hotel","hour","house","how","however","huge","human",
|
||||
"hundred","husband","I","idea","identify","if","image","imagine","impact",
|
||||
"important","improve","in","include","including","increase","indeed","indicate",
|
||||
"individual","industry","information","inside","instead","institution","interest",
|
||||
"interesting","international","interview","into","investment","involve","issue",
|
||||
"it","item","its","itself","job","join","just","keep","key","kid","kill","kind",
|
||||
"kitchen","know","knowledge","land","language","large","last","late","later",
|
||||
"laugh","law","lawyer","lay","lead","leader","learn","least","leave","left",
|
||||
"leg","legal","less","let","letter","level","lie","life","light","like","likely",
|
||||
"line","list","listen","little","live","local","long","look","lose","loss",
|
||||
"lot","love","low","machine","magazine","main","maintain","major","majority",
|
||||
"make","man","manage","management","manager","many","market","marriage",
|
||||
"material","matter","may","maybe","me","mean","measure","media","medical","meet",
|
||||
"meeting","member","memory","mention","message","method","middle","might",
|
||||
"military","million","mind","minute","miss","mission","model","modern","moment",
|
||||
"money","month","more","morning","most","mother","mouth","move","movement",
|
||||
"movie","Mr","Mrs","much","music","must","my","myself","name","nation",
|
||||
"national","natural","nature","near","nearly","necessary","need","network",
|
||||
"never","new","news","newspaper","next","nice","night","no","none","nor",
|
||||
"north","not","note","nothing","notice","now","number","occur","of","off",
|
||||
"offer","office","officer","official","often","oh","oil","ok","old","on",
|
||||
"once","one","only","onto","open","operation","opportunity","option","or",
|
||||
"order","organization","other","others","our","out","outside","over","own",
|
||||
"owner","page","pain","painting","paper","parent","part","participant",
|
||||
"particular","particularly","partner","party","pass","past","patient","pattern",
|
||||
"pay","peace","people","per","perform","performance","perhaps","period",
|
||||
"person","personal","phone","physical","pick","picture","piece","place","plan",
|
||||
"plant","play","player","point","police","policy","political","politics",
|
||||
"poor","popular","population","position","positive","possible","power",
|
||||
"practice","prepare","present","president","pressure","pretty","prevent","price",
|
||||
"private","probably","problem","process","produce","product","production",
|
||||
"professional","professor","program","project","property","protect","prove",
|
||||
"provide","public","pull","purpose","push","put","quality","question","quickly",
|
||||
"quite","race","radio","raise","range","rate","rather","reach","read","ready",
|
||||
"real","reality","realize","really","reason","receive","recent","recently",
|
||||
"recognize","record","red","reduce","reflect","region","relate","relationship",
|
||||
"religious","remain","remember","remove","report","represent","republican",
|
||||
"require","research","resource","respond","response","responsibility","rest",
|
||||
"result","return","reveal","rich","right","rise","risk","road","rock","role",
|
||||
"room","rule","run","safe","same","save","say","scene","school","science",
|
||||
"scientist","score","sea","season","seat","second","section","security","see",
|
||||
"seek","seem","sell","send","senior","sense","series","serious","serve",
|
||||
"service","set","seven","several","shake","share","she","shoot","short","shot",
|
||||
"should","shoulder","show","side","sign","significant","similar","simple",
|
||||
"simply","since","sing","single","sister","sit","site","situation","six","size",
|
||||
"skill","skin","small","smile","so","social","society","soldier","some",
|
||||
"somebody","someone","something","sometimes","son","song","soon","sort","sound",
|
||||
"source","south","southern","space","speak","special","specific","speech",
|
||||
"spend","sport","spring","staff","stage","stand","standard","star","start",
|
||||
"state","statement","station","stay","step","still","stock","stop","store",
|
||||
"story","strategy","street","strong","structure","student","study","stuff",
|
||||
"style","subject","success","successful","such","suddenly","suffer","suggest",
|
||||
"summer","support","sure","surface","system","table","take","talk","task","tax",
|
||||
"teach","teacher","team","technology","television","tell","ten","tend","term",
|
||||
"test","than","thank","that","the","their","them","themselves","then","theory",
|
||||
"there","these","they","thing","think","third","this","those","though","thought",
|
||||
"thousand","threat","three","through","throughout","throw","thus","time","to",
|
||||
"today","together","tonight","too","top","total","tough","toward","town","trade",
|
||||
"traditional","training","travel","treat","treatment","tree","trial","trip",
|
||||
"trouble","true","truth","try","turn","TV","two","type","under","understand",
|
||||
"unit","until","up","upon","us","use","usually","value","various","very",
|
||||
"victim","view","violence","visit","voice","vote","wait","walk","wall","want",
|
||||
"war","watch","water","way","we","weapon","wear","week","weight","well","west",
|
||||
"western","what","whatever","when","where","whether","which","while","white",
|
||||
"who","whole","whom","whose","why","wide","wife","will","win","wind","window",
|
||||
"wish","with","within","without","woman","wonder","word","work","worker","world",
|
||||
"worry","would","write","writer","wrong","yard","yeah","year","yes","yet","you",
|
||||
"young","your","yourself","meshtastic","node","lora","mesh"]
|
||||
|
||||
def __init__(self):
|
||||
self.game = {}
|
||||
|
||||
def new_game(self, id):
|
||||
games = won = 0
|
||||
ret = ""
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
ret += f"Total Games: {games}, Won: {won}\n"
|
||||
|
||||
self.game[id] = {
|
||||
"word": self.random_word(),
|
||||
"guesses": [],
|
||||
"games": games+1,
|
||||
"won": won
|
||||
}
|
||||
ret += self.game_continue(id)
|
||||
return ret
|
||||
|
||||
def guess(self, id, input):
|
||||
g = self.game[id]
|
||||
if not input:
|
||||
return
|
||||
letter = input[0].lower()
|
||||
if letter.isalpha() and letter not in g["guesses"]:
|
||||
g["guesses"].append(letter)
|
||||
|
||||
def wrong_guesses(self, id):
|
||||
g = self.game[id]
|
||||
wrong = 0
|
||||
for letter in g["guesses"]:
|
||||
if letter not in g["word"]:
|
||||
wrong += 1
|
||||
return wrong
|
||||
|
||||
def won(self, id):
|
||||
g = self.game[id]
|
||||
for letter in g["word"]:
|
||||
if letter not in g["guesses"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def mask(self, id):
|
||||
g = self.game[id]
|
||||
return " ".join([a if a in g["guesses"] else "_" for a in g["word"]])
|
||||
|
||||
def game_board(self, id):
|
||||
g = self.game[id]
|
||||
emotions = "😀🙂😐😑😕😔💀"
|
||||
wrong = self.wrong_guesses(id)
|
||||
ret = ""
|
||||
if self.won(id):
|
||||
ret += "🥳" + "\n"
|
||||
g["won"] += 1
|
||||
else:
|
||||
ret += emotions[wrong] + "\n"
|
||||
ret += hangman.mask(id) + "\n"
|
||||
if g["guesses"]:
|
||||
ret += ",".join(g["guesses"]) + "\n"
|
||||
return ret
|
||||
|
||||
def game_continue(self, id):
|
||||
return self.game_board(id) + "Guess a letter"
|
||||
|
||||
def game_over(self, id):
|
||||
return self.game_board(id) + "Game over, the word was " + self.game[id]["word"]
|
||||
|
||||
def play(self, id, input):
|
||||
if id not in self.game:
|
||||
return self.new_game(id)
|
||||
self.guess(id, input)
|
||||
wrong = self.wrong_guesses(id)
|
||||
if wrong >= 6 or self.won(id):
|
||||
return self.game_over(id) + "\n" + self.new_game(id)
|
||||
return self.game_continue(id)
|
||||
|
||||
def end(self, id):
|
||||
del self.game[id]
|
||||
|
||||
def random_word(self):
|
||||
return random.choice(self.WORDS)
|
||||
|
||||
hangmanTracker = []
|
||||
hangman = Hangman()
|
||||
@@ -117,6 +117,10 @@ def sendWithEmoji(message):
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
dadjoke = Dadjoke()
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ class PlayerVP:
|
||||
return self.show_hand()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
|
||||
|
||||
# Method for scoring hand, calculating winnings, and outputting message
|
||||
@@ -390,7 +390,7 @@ def playVideoPoker(nodeID, message):
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
drawCount += 1
|
||||
@@ -403,7 +403,7 @@ def playVideoPoker(nodeID, message):
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
|
||||
77
modules/globalalert.py
Normal file
77
modules/globalalert.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# helper functions to use location data for data outside US/north america
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
from geopy.geocoders import Nominatim # pip install geopy
|
||||
import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
trap_list_location_de = ("dealert", "dewx", "deflood")
|
||||
|
||||
def get_govUK_alerts(lat, lon):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
|
||||
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK alerts: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
if alert:
|
||||
return "🚨" + alert.get_text(strip=True)
|
||||
else:
|
||||
return NO_ALERTS
|
||||
|
||||
def get_nina_alerts():
|
||||
try:
|
||||
# get api.bund.dev alerts
|
||||
alerts = []
|
||||
for regionalKey in myRegionalKeysDE:
|
||||
url = ("https://nina.api.proxy.bund.dev/api31/dashboard/" + regionalKey + ".json")
|
||||
response = requests.get(url)
|
||||
data = response.json()
|
||||
|
||||
for item in data:
|
||||
title = item["i18nTitle"]["de"]
|
||||
alerts.append(f"🚨 {title}")
|
||||
return "\n".join(alerts) if alerts else NO_ALERTS
|
||||
except Exception as e:
|
||||
logger.warning("Error getting NINA DE alerts: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
def get_wxUKgov():
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
|
||||
try:
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.content, 'xml')
|
||||
|
||||
items = soup.find_all('item')
|
||||
alerts = []
|
||||
|
||||
for item in items:
|
||||
title = item.find('title').get_text(strip=True)
|
||||
description = item.find('description').get_text(strip=True)
|
||||
alerts.append(f"🚨 {title}: {description}")
|
||||
|
||||
return "\n".join(alerts) if alerts else NO_ALERTS
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK weather warnings: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
73
modules/gpio.py
Normal file
73
modules/gpio.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# GPIO module for MeshLink, concept code, not implemented
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
# https://pypi.org/project/gpio/
|
||||
#import gpio
|
||||
|
||||
# https://pythonhosted.org/RPIO/
|
||||
import RPIO
|
||||
|
||||
from modules.log import *
|
||||
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
|
||||
|
||||
# set up input channel without pull-up
|
||||
RPIO.setup(7, RPIO.IN)
|
||||
|
||||
# set up input channel with pull-up
|
||||
RPIO.setup(8, RPIO.IN, pull_up_down=RPIO.PUD_UP)
|
||||
|
||||
# set up GPIO output channel
|
||||
RPIO.setup(8, RPIO.OUT)
|
||||
|
||||
# change to BOARD numbering schema
|
||||
RPIO.setmode(RPIO.BOARD)
|
||||
|
||||
# set up PWM channel
|
||||
RPIO.setup(12, RPIO.OUT)
|
||||
p = RPIO.PWM(12)
|
||||
|
||||
def gpio_status():
|
||||
# get status of GPIO pins
|
||||
gpio_status = ""
|
||||
gpio_status += "GPIO 7: " + str(RPIO.input(7)) + "\n"
|
||||
gpio_status += "GPIO 8: " + str(RPIO.input(8)) + "\n"
|
||||
gpio_status += "GPIO 12: " + str(RPIO.input(12)) + "\n"
|
||||
return gpio_status
|
||||
|
||||
def gpio_toggle():
|
||||
# toggle GPIO pin 8
|
||||
RPIO.output(8, not RPIO.input(8))
|
||||
return "GPIO 8 toggled"
|
||||
|
||||
def gpio_pwm():
|
||||
# set PWM on GPIO pin 12
|
||||
p.start(50)
|
||||
return "PWM started"
|
||||
|
||||
def gpio_stop():
|
||||
# stop PWM on GPIO pin 12
|
||||
p.stop()
|
||||
return "PWM stopped"
|
||||
|
||||
def gpio_shutdown():
|
||||
# shutdown GPIO
|
||||
RPIO.cleanup()
|
||||
return "GPIO shutdown"
|
||||
|
||||
def trap_gpio(message):
|
||||
# trap for GPIO commands
|
||||
if "status" in message:
|
||||
return gpio_status()
|
||||
elif "toggle" in message:
|
||||
return gpio_toggle()
|
||||
elif "pwm" in message:
|
||||
return gpio_pwm()
|
||||
elif "stop" in message:
|
||||
return gpio_stop()
|
||||
elif "shutdown" in message:
|
||||
return gpio_shutdown()
|
||||
else:
|
||||
return "GPIO command not recognized"
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# LLM Module for meshing-around
|
||||
# This module is used to interact with Ollama to generate responses to user input
|
||||
# This module is used to interact with LLM API to generate responses to user input
|
||||
# K7MHI Kelly Keeton 2024
|
||||
from modules.log import *
|
||||
|
||||
# Ollama Client
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
from ollama import Client as OllamaClient
|
||||
import requests
|
||||
import json
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# This is my attempt at a simple RAG implementation it will require some setup
|
||||
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
|
||||
# This is lighter weight and can be used in a standalone environment, needs chromadb
|
||||
# "chat with a file" is the use concept here, the file is the RAG data
|
||||
# is anyone using this please let me know if you are Dec62024 -kelly
|
||||
ragDEV = False
|
||||
|
||||
if ragDEV:
|
||||
import os
|
||||
import ollama # pip install ollama
|
||||
import chromadb # pip install chromadb
|
||||
from ollama import Client as OllamaClient
|
||||
ollamaClient = OllamaClient(host=ollamaHostName)
|
||||
|
||||
# LLM System Variables
|
||||
ollamaClient = OllamaClient(host=ollamaHostName)
|
||||
ollamaAPI = ollamaHostName + "/api/generate"
|
||||
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
|
||||
llmEnableHistory = True # enable last message history for the LLM model
|
||||
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
@@ -193,20 +198,30 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model with RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
# Condense the result to just needed
|
||||
if isinstance(result, dict):
|
||||
result = result.get("response")
|
||||
else:
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model without RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
|
||||
# Condense the result to just needed
|
||||
if isinstance(result, dict):
|
||||
result = result.get("response")
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False}
|
||||
# Query the model via Ollama web API
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
# Condense the result to just needed
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "I am having trouble processing your request, please try again later."
|
||||
return "⛔️I am having trouble processing your request, please try again later."
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
@@ -217,15 +232,3 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
llmChat_history[nodeID] = [input, response]
|
||||
|
||||
return response
|
||||
|
||||
# import subprocess
|
||||
# def get_ollama_cpu():
|
||||
# try:
|
||||
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
|
||||
# if "GPU" in psOutput.stdout:
|
||||
# logger.debug(f"System: Ollama process with GPU")
|
||||
# else:
|
||||
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
|
||||
# except Exception as e:
|
||||
# logger.debug(f"System: Ollama process not found, {e}")
|
||||
# return False
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# helper functions to use location data like NOAA weather
|
||||
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow","valert")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -71,7 +71,16 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
def getRepeaterBook(lat=0, lon=0):
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
data = []
|
||||
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
# check if in the US or not
|
||||
usapi ="https://www.repeaterbook.com/repeaters/prox_result.php?"
|
||||
elsewhereapi = "https://www.repeaterbook.com/row_repeaters/prox2_result.php?"
|
||||
if grid[:2] in ['CN', 'DN', 'EN', 'FN', 'CM', 'DM', 'EM', 'FM', 'DL', 'EL', 'FL']:
|
||||
repeater_url = usapi
|
||||
else:
|
||||
repeater_url = elsewhereapi
|
||||
|
||||
repeater_url += f"city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
|
||||
try:
|
||||
msg = ''
|
||||
response = requests.get(repeater_url)
|
||||
@@ -95,10 +104,8 @@ def getRepeaterBook(lat=0, lon=0):
|
||||
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
|
||||
}
|
||||
data.append(repeater)
|
||||
else:
|
||||
msg = "bug?Not enough columns"
|
||||
else:
|
||||
msg = "bug?Table not found"
|
||||
msg = "No Data for your Region"
|
||||
except Exception as e:
|
||||
msg = "No repeaters found 😔"
|
||||
# Limit the output to the first 4 repeaters
|
||||
@@ -154,9 +161,8 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
else:
|
||||
msg = f"no results.. sorry"
|
||||
return msg
|
||||
|
||||
|
||||
def get_tide(lat=0, lon=0):
|
||||
def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
@@ -172,7 +178,7 @@ def get_tide(lat=0, lon=0):
|
||||
|
||||
if station_json['stationList'] == [] or station_json['stationList'] is None:
|
||||
logger.error("Location:No tide station found")
|
||||
return ERROR_FETCHING_DATA
|
||||
return "No tide station found with info provided"
|
||||
|
||||
station_id = station_json['stationList'][0]['stationId']
|
||||
|
||||
@@ -219,7 +225,7 @@ def get_tide(lat=0, lon=0):
|
||||
tide_table = tide_table[:-1]
|
||||
return tide_table
|
||||
|
||||
def get_weather(lat=0, lon=0, unit=0):
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
weather = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -228,42 +234,43 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
unit = 1
|
||||
logger.debug("Location: new API metric units not implemented yet")
|
||||
|
||||
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
|
||||
if unit == 1:
|
||||
weather_url += "&unit=1"
|
||||
|
||||
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
|
||||
# extract the "forecast": property from the JSON response
|
||||
try:
|
||||
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
|
||||
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
|
||||
if not weather_data.ok:
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
# get the forecast URL from the JSON response
|
||||
weather_json = weather_data.json()
|
||||
forecast_url = weather_json['properties']['forecast']
|
||||
try:
|
||||
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
|
||||
if not forecast_data.ok:
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
|
||||
table = soup.find('div', id="detailed-forecast-body")
|
||||
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
|
||||
forecast_json = forecast_data.json()
|
||||
forecast = forecast_json['properties']['periods']
|
||||
for day in forecast[:forecastDuration]:
|
||||
# abreviate the forecast
|
||||
|
||||
if table is None:
|
||||
logger.error("Location:Bad weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
# get rows
|
||||
rows = table.find_all('div', class_="row")
|
||||
|
||||
# extract data from rows
|
||||
for row in rows:
|
||||
# shrink the text
|
||||
line = abbreviate_weather(row.text)
|
||||
# only grab a few days of weather
|
||||
if len(weather.split("\n")) < forecastDuration:
|
||||
weather += line + "\n"
|
||||
# trim off last newline
|
||||
weather += abbreviate_noaa(day['name']) + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
|
||||
# remove last newline
|
||||
weather = weather[:-1]
|
||||
|
||||
# get any alerts and return the count
|
||||
alerts = getWeatherAlerts(lat, lon)
|
||||
alerts = getWeatherAlertsNOAA(lat, lon)
|
||||
|
||||
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
|
||||
alert = ""
|
||||
@@ -278,23 +285,16 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather
|
||||
|
||||
def abbreviate_weather(row):
|
||||
def abbreviate_noaa(row):
|
||||
# replace long strings with shorter ones for display
|
||||
replacements = {
|
||||
"Monday": "Mon ",
|
||||
"Tuesday": "Tue ",
|
||||
"Wednesday": "Wed ",
|
||||
"Thursday": "Thu ",
|
||||
"Friday": "Fri ",
|
||||
"Saturday": "Sat ",
|
||||
"Sunday": "Sunday ",
|
||||
"Today": "Today ",
|
||||
"Night": "Night ",
|
||||
"Tonight": "Tonight ",
|
||||
"Tomorrow": "Tomorrow ",
|
||||
"Day": "Day ",
|
||||
"This Afternoon": "Afternoon ",
|
||||
"Overnight": "Overnight ",
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun",
|
||||
"northwest": "NW",
|
||||
"northeast": "NE",
|
||||
"southwest": "SW",
|
||||
@@ -303,29 +303,40 @@ def abbreviate_weather(row):
|
||||
"south": "S",
|
||||
"east": "E",
|
||||
"west": "W",
|
||||
"Northwest": "NW",
|
||||
"Northeast": "NE",
|
||||
"Southwest": "SW",
|
||||
"Southeast": "SE",
|
||||
"North": "N",
|
||||
"South": "S",
|
||||
"East": "E",
|
||||
"West": "W",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
"thunderstorm": "t-storm",
|
||||
"quarters": "qtrs",
|
||||
"quarter": "qtr"
|
||||
"quarter": "qtr",
|
||||
"january": "Jan",
|
||||
"february": "Feb",
|
||||
"march": "Mar",
|
||||
"april": "Apr",
|
||||
"may": "May",
|
||||
"june": "Jun",
|
||||
"july": "Jul",
|
||||
"august": "Aug",
|
||||
"september": "Sep",
|
||||
"october": "Oct",
|
||||
"november": "Nov",
|
||||
"december": "Dec",
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
}
|
||||
|
||||
line = row
|
||||
for key, value in replacements.items():
|
||||
line = line.replace(key, value)
|
||||
# case insensitive replace
|
||||
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
|
||||
|
||||
return line
|
||||
|
||||
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
@@ -350,11 +361,13 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
|
||||
alerts = ""
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
|
||||
for i in alertxml.getElementsByTagName("entry"):
|
||||
alerts += (
|
||||
i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n"
|
||||
)
|
||||
title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue
|
||||
area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue
|
||||
if enableExtraLocationWx:
|
||||
alerts += f"{title}. {area_desc.replace(' ', '')}\n"
|
||||
else:
|
||||
alerts += f"{title}\n"
|
||||
|
||||
if alerts == "" or alerts == None:
|
||||
return NO_ALERTS
|
||||
@@ -367,30 +380,39 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
alert_num = 0
|
||||
alert_num = len(alerts.split("\n"))
|
||||
|
||||
alerts = abbreviate_weather(alerts)
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# return the first ALERT_COUNT alerts
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCache = ""
|
||||
def alertBrodcast():
|
||||
wxAlertCacheNOAA = ""
|
||||
def alertBrodcastNOAA():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCache
|
||||
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
|
||||
|
||||
if currentAlert[0] == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
|
||||
wxAlertCache = ""
|
||||
global wxAlertCacheNOAA
|
||||
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
|
||||
# check if any reason to discard the alerts
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
|
||||
return False
|
||||
elif currentAlert == NO_ALERTS:
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
if ignoreEASenable:
|
||||
# check if the alert is in the ignoreEAS list
|
||||
for word in ignoreEASwords:
|
||||
if word.lower() in currentAlert[0].lower():
|
||||
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] != wxAlertCache:
|
||||
elif currentAlert[0] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
logger.debug("Location:Broadcasting weather alerts")
|
||||
wxAlertCache = currentAlert[0]
|
||||
wxAlertCacheNOAA = currentAlert[0]
|
||||
return currentAlert
|
||||
|
||||
return False
|
||||
|
||||
def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -425,7 +447,7 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
"\n***\n"
|
||||
)
|
||||
|
||||
alerts = abbreviate_weather(alerts)
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# trim the alerts to the first ALERT_COUNT
|
||||
alerts = alerts.split("\n***\n")[:numWxAlerts]
|
||||
@@ -440,3 +462,226 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
alerts = "\n".join(alerts)
|
||||
|
||||
return alerts
|
||||
|
||||
def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# get the latest IPAWS alert from FEMA
|
||||
alert = ''
|
||||
alerts = []
|
||||
linked_data = ''
|
||||
|
||||
# set the API URL for IPAWS
|
||||
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
|
||||
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
|
||||
if ipawsPIN != "000000":
|
||||
alert_url += "?pin=" + ipawsPIN
|
||||
|
||||
# get the alerts from FEMA
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
|
||||
# extract alerts from main feed
|
||||
for entry in alertxml.getElementsByTagName("entry"):
|
||||
link = entry.getElementsByTagName("link")[0].getAttribute("href")
|
||||
try:
|
||||
#pin check
|
||||
if ipawsPIN != "000000":
|
||||
link += "?pin=" + ipawsPIN
|
||||
# get the linked alert data from FEMA
|
||||
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
|
||||
if not linked_data.ok or not linked_data.text.strip():
|
||||
# if the linked data is not ok, skip this alert
|
||||
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
|
||||
continue
|
||||
else:
|
||||
linked_xml = xml.dom.minidom.parseString(linked_data.text)
|
||||
# this alert is a full CAP alert
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
|
||||
continue
|
||||
except xml.parsers.expat.ExpatError:
|
||||
logger.warning(f"System: iPAWS Error parsing XML from {link}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"System: iPAWS Error processing alert data from {link}: {e}")
|
||||
continue
|
||||
|
||||
for info in linked_xml.getElementsByTagName("info"):
|
||||
# extract values from XML
|
||||
sameVal = "NONE"
|
||||
geocode_value = "NONE"
|
||||
description = ""
|
||||
try:
|
||||
eventCode_table = info.getElementsByTagName("eventCode")[0]
|
||||
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
|
||||
# use headline if no description
|
||||
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
else:
|
||||
description = headline
|
||||
|
||||
area_table = info.getElementsByTagName("area")[0]
|
||||
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
|
||||
|
||||
geocode_table = area_table.getElementsByTagName("geocode")[0]
|
||||
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
except Exception as e:
|
||||
logger.debug(f"System: iPAWS Error extracting alert data: {link}")
|
||||
#print(f"DEBUG: {info.toprettyxml()}")
|
||||
continue
|
||||
|
||||
# check if the alert is for the current location, if wanted keep alert
|
||||
if (sameVal in mySAME) or (geocode_value in mySAME):
|
||||
# ignore the FEMA test alerts
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Ignoring FEMA Alert: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
alerts.append({
|
||||
'alertType': alertType,
|
||||
'alertCode': alertCode,
|
||||
'headline': headline,
|
||||
'areaDesc': areaDesc,
|
||||
'geocode_type': geocode_type,
|
||||
'geocode_value': geocode_value,
|
||||
'description': description
|
||||
})
|
||||
# else:
|
||||
# # these are discarded some day but logged for debugging currently
|
||||
# logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
for alertItem in alerts[:numWxAlerts]:
|
||||
if shortAlerts:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}")
|
||||
else:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}\n{alertItem['description']}")
|
||||
# add a newline if not the last alert
|
||||
if alertItem != alerts[:numWxAlerts][-1]:
|
||||
alert += "\n"
|
||||
else:
|
||||
alert = NO_ALERTS
|
||||
|
||||
return alert
|
||||
|
||||
def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
# get the latest flood alert from NOAA
|
||||
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
|
||||
headers = {'accept': 'application/json'}
|
||||
if uid == 0:
|
||||
return "No flood gauge data found"
|
||||
try:
|
||||
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
|
||||
if not response.ok:
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
data = response.json()
|
||||
if not data:
|
||||
return "No flood gauge data found"
|
||||
|
||||
# extract values from JSON
|
||||
try:
|
||||
name = data['name']
|
||||
status_observed_primary = data['status']['observed']['primary']
|
||||
status_observed_primary_unit = data['status']['observed']['primaryUnit']
|
||||
status_observed_secondary = data['status']['observed']['secondary']
|
||||
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
|
||||
status_observed_floodCategory = data['status']['observed']['floodCategory']
|
||||
status_forecast_primary = data['status']['forecast']['primary']
|
||||
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
|
||||
status_forecast_secondary = data['status']['forecast']['secondary']
|
||||
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
|
||||
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
|
||||
|
||||
# except KeyError as e:
|
||||
# print(f"Missing key in data: {e}")
|
||||
# except TypeError as e:
|
||||
# print(f"Type error in data: {e}")
|
||||
except Exception as e:
|
||||
logger.debug("Location:Error extracting flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# format the flood data
|
||||
logger.debug(f"System: NOAA Flood data for {str(uid)}")
|
||||
flood_data = f"Flood Data {name}:\n"
|
||||
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
|
||||
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
|
||||
|
||||
return flood_data
|
||||
|
||||
def get_volcano_usgs(lat=0, lon=0):
|
||||
alerts = ''
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
# get the latest volcano alert from USGS from CAP feed
|
||||
usgs_volcano_url = "https://volcanoes.usgs.gov/hans-public/api/volcano/getCapElevated"
|
||||
try:
|
||||
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
|
||||
if not volcano_data.ok:
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
volcano_json = volcano_data.json()
|
||||
# extract alerts from main feed
|
||||
if volcano_json and isinstance(volcano_json, list):
|
||||
for alert in volcano_json:
|
||||
# check ignore list
|
||||
if ignoreUSGSEnable:
|
||||
for word in ignoreUSGSwords:
|
||||
if word.lower() in alert['volcano_name_appended'].lower():
|
||||
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
|
||||
continue
|
||||
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
|
||||
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
|
||||
volcano_name = alert['volcano_name_appended']
|
||||
alert_level = alert['alert_level']
|
||||
color_code = alert['color_code']
|
||||
cap_severity = alert['cap_severity']
|
||||
synopsis = alert['synopsis']
|
||||
# format Alert
|
||||
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
|
||||
else:
|
||||
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
|
||||
continue
|
||||
else:
|
||||
logger.debug("Location:Error fetching volcano data from USGS")
|
||||
return NO_ALERTS
|
||||
if alerts == "":
|
||||
return NO_ALERTS
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
alerts = alerts[:-1]
|
||||
# return the alerts
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
return alerts
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime
|
||||
from modules.settings import *
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
LOGGING_LEVEL = "DEBUG"
|
||||
|
||||
LOGGING_LEVEL = getattr(logging, LOGGING_LEVEL)
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
grey = '\x1b[38;21m'
|
||||
@@ -41,7 +46,7 @@ class plainFormatter(logging.Formatter):
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.setLevel(LOGGING_LEVEL)
|
||||
logger.propagate = False
|
||||
|
||||
msgLogger = logging.getLogger("MeshBot Messages Logger")
|
||||
@@ -56,7 +61,7 @@ today = datetime.now()
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
# Set level for stdout handler (logs DEBUG level and above)
|
||||
stdout_handler.setLevel(logging.DEBUG)
|
||||
stdout_handler.setLevel(LOGGING_LEVEL)
|
||||
# Set format for stdout handler
|
||||
stdout_handler.setFormatter(CustomFormatter(logFormat))
|
||||
# Add handlers to the logger
|
||||
@@ -64,14 +69,32 @@ logger.addHandler(stdout_handler)
|
||||
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
# Pretty Timestamp
|
||||
def getPrettyTime(seconds):
|
||||
# convert unix time to minutes, hours, or days, or years for simple display
|
||||
designator = "s"
|
||||
if seconds > 0:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "m"
|
||||
if seconds > 60:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "h"
|
||||
if seconds > 24:
|
||||
seconds = round(seconds / 24)
|
||||
designator = "d"
|
||||
if seconds > 365:
|
||||
seconds = round(seconds / 365)
|
||||
designator = "y"
|
||||
return str(seconds) + designator
|
||||
57
modules/qrz.py
Normal file
57
modules/qrz.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Module to respomnd to new nodes we havent seen before with a hello message
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
|
||||
def initalize_qrz_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
# Check if the qrz table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS qrz
|
||||
(qrz_id INTEGER PRIMARY KEY, qrz_call TEXT, qrz_name TEXT, qrz_qth TEXT, qrz_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def never_seen_before(nodeID):
|
||||
# check if we have seen this node before and sent a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row is None:
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
# we have seen this node before
|
||||
return False
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
logger.warning("QRZ database table not found, created new table")
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
|
||||
def hello(nodeID, name):
|
||||
# send a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import configparser
|
||||
# messages
|
||||
NO_DATA_NOGPS = "No location data: does your device have GPS?"
|
||||
ERROR_FETCHING_DATA = "error fetching data"
|
||||
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd? for more'
|
||||
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, CMD? for more'
|
||||
EMERGENCY_RESPONSE = "MeshBot detected a possible request for Emergency Assistance and alerted a wider audience."
|
||||
MOTD = 'Thanks for using MeshBOT! Have a good day!'
|
||||
NO_ALERTS = "No weather alerts found."
|
||||
NO_ALERTS = "No alerts found."
|
||||
|
||||
# setup the global variables
|
||||
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
|
||||
@@ -24,65 +25,85 @@ max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
scheduler_enabled = False # enable the scheduler currently config via code only
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
playingGame = False
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
config_file = "config.ini"
|
||||
|
||||
try:
|
||||
config.read(config_file)
|
||||
config.read(config_file, encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
if config.sections() == []:
|
||||
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
|
||||
config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''}
|
||||
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD,
|
||||
'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
|
||||
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
print (f"System: Config file created, check {config_file} or review the config.template")
|
||||
|
||||
if 'sentry' not in config:
|
||||
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'location' not in config:
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'bbs' not in config:
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'repeater' not in config:
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'radioMon' not in config:
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'games' not in config:
|
||||
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'messagingSettings' not in config:
|
||||
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'fileMon' not in config:
|
||||
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'scheduler' not in config:
|
||||
config['scheduler'] = {'enabled': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'emergencyHandler' not in config:
|
||||
config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'smtp' not in config:
|
||||
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'checklist' not in config:
|
||||
config['checklist'] = {'enabled': 'False', 'checklist_db': 'data/checklist.db'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'qrz' not in config:
|
||||
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
hostname1 = config['interface'].get('hostname', '')
|
||||
mac1 = config['interface'].get('mac', '')
|
||||
interface1_enabled = True # gotta have at least one interface
|
||||
|
||||
# interface2 settings
|
||||
if 'interface2' in config:
|
||||
@@ -94,16 +115,94 @@ if 'interface2' in config:
|
||||
else:
|
||||
interface2_enabled = False
|
||||
|
||||
# variables
|
||||
# interface3 settings
|
||||
if 'interface3' in config:
|
||||
interface3_type = config['interface3'].get('type', 'serial')
|
||||
port3 = config['interface3'].get('port', '')
|
||||
hostname3 = config['interface3'].get('hostname', '')
|
||||
mac3 = config['interface3'].get('mac', '')
|
||||
interface3_enabled = config['interface3'].getboolean('enabled', False)
|
||||
else:
|
||||
interface3_enabled = False
|
||||
|
||||
# interface4 settings
|
||||
if 'interface4' in config:
|
||||
interface4_type = config['interface4'].get('type', 'serial')
|
||||
port4 = config['interface4'].get('port', '')
|
||||
hostname4 = config['interface4'].get('hostname', '')
|
||||
mac4 = config['interface4'].get('mac', '')
|
||||
interface4_enabled = config['interface4'].getboolean('enabled', False)
|
||||
else:
|
||||
interface4_enabled = False
|
||||
|
||||
# interface5 settings
|
||||
if 'interface5' in config:
|
||||
interface5_type = config['interface5'].get('type', 'serial')
|
||||
port5 = config['interface5'].get('port', '')
|
||||
hostname5 = config['interface5'].get('hostname', '')
|
||||
mac5 = config['interface5'].get('mac', '')
|
||||
interface5_enabled = config['interface5'].getboolean('enabled', False)
|
||||
else:
|
||||
interface5_enabled = False
|
||||
|
||||
# interface6 settings
|
||||
if 'interface6' in config:
|
||||
interface6_type = config['interface6'].get('type', 'serial')
|
||||
port6 = config['interface6'].get('port', '')
|
||||
hostname6 = config['interface6'].get('hostname', '')
|
||||
mac6 = config['interface6'].get('mac', '')
|
||||
interface6_enabled = config['interface6'].getboolean('enabled', False)
|
||||
else:
|
||||
interface6_enabled = False
|
||||
|
||||
# interface7 settings
|
||||
if 'interface7' in config:
|
||||
interface7_type = config['interface7'].get('type', 'serial')
|
||||
port7 = config['interface7'].get('port', '')
|
||||
hostname7 = config['interface7'].get('hostname', '')
|
||||
mac7 = config['interface7'].get('mac', '')
|
||||
interface7_enabled = config['interface7'].getboolean('enabled', False)
|
||||
else:
|
||||
interface7_enabled = False
|
||||
|
||||
# interface8 settings
|
||||
if 'interface8' in config:
|
||||
interface8_type = config['interface8'].get('type', 'serial')
|
||||
port8 = config['interface8'].get('port', '')
|
||||
hostname8 = config['interface8'].get('hostname', '')
|
||||
mac8 = config['interface8'].get('mac', '')
|
||||
interface8_enabled = config['interface8'].getboolean('enabled', False)
|
||||
else:
|
||||
interface8_enabled = False
|
||||
|
||||
# interface9 settings
|
||||
if 'interface9' in config:
|
||||
interface9_type = config['interface9'].get('type', 'serial')
|
||||
port9 = config['interface9'].get('port', '')
|
||||
hostname9 = config['interface9'].get('hostname', '')
|
||||
mac9 = config['interface9'].get('mac', '')
|
||||
interface9_enabled = config['interface9'].getboolean('enabled', False)
|
||||
else:
|
||||
interface9_enabled = False
|
||||
|
||||
multiple_interface = False
|
||||
if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled:
|
||||
multiple_interface = True
|
||||
|
||||
|
||||
# variables from the config.ini file
|
||||
try:
|
||||
# general
|
||||
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
|
||||
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
|
||||
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
cmdBang = config['general'].getboolean('cmdBang', False) # default off
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
|
||||
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
|
||||
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True)
|
||||
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
|
||||
@@ -111,15 +210,24 @@ try:
|
||||
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
|
||||
motd_enabled = config['general'].getboolean('motdEnabled', True)
|
||||
MOTD = config['general'].get('motd', MOTD)
|
||||
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
|
||||
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
|
||||
bee_enabled = config['general'].getboolean('bee', False) # 🐝 off by default undocumented
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True)
|
||||
# emergency response
|
||||
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
|
||||
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
|
||||
emergency_responder_alert_interface = config['emergencyHandler'].getint('alert_interface', 1) # default 1
|
||||
emergency_responder_email = config['emergencyHandler'].get('email', '').split(',')
|
||||
|
||||
# sentry
|
||||
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
|
||||
@@ -127,6 +235,10 @@ try:
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
|
||||
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
|
||||
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -134,29 +246,82 @@ try:
|
||||
longitudeValue = config['location'].getfloat('lon', -123.0)
|
||||
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
|
||||
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
|
||||
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
|
||||
# location alerts
|
||||
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
|
||||
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
|
||||
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
if ',' in wxAlertBroadcastChannel:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
|
||||
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
|
||||
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
|
||||
volcanoAlertBroadcastChannel = config['location'].get('volcanoAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
|
||||
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
|
||||
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
||||
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
|
||||
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
|
||||
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
|
||||
|
||||
# checklist
|
||||
checklist_enabled = config['checklist'].getboolean('enabled', False)
|
||||
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
|
||||
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
|
||||
|
||||
# qrz hello
|
||||
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
|
||||
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
|
||||
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
|
||||
train_qrz = config['qrz'].getboolean('training', True)
|
||||
|
||||
# E-Mail Settings
|
||||
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
||||
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
|
||||
enableImap = config['smtp'].getboolean('enableImap', False)
|
||||
SMTP_SERVER = config['smtp'].get('SMTP_SERVER', 'smtp.gmail.com')
|
||||
SMTP_PORT = config['smtp'].getint('SMTP_PORT', 587)
|
||||
FROM_EMAIL = config['smtp'].get('FROM_EMAIL', 'none@gmail.com')
|
||||
SMTP_AUTH = config['smtp'].getboolean('SMTP_AUTH', True)
|
||||
SMTP_USERNAME = config['smtp'].get('SMTP_USERNAME', FROM_EMAIL)
|
||||
SMTP_PASSWORD = config['smtp'].get('SMTP_PASSWORD', 'password')
|
||||
EMAIL_SUBJECT = config['smtp'].get('EMAIL_SUBJECT', 'Meshtastic✉️')
|
||||
IMAP_SERVER = config['smtp'].get('IMAP_SERVER', 'imap.gmail.com')
|
||||
IMAP_PORT = config['smtp'].getint('IMAP_PORT', 993)
|
||||
IMAP_USERNAME = config['smtp'].get('IMAP_USERNAME', SMTP_USERNAME)
|
||||
IMAP_PASSWORD = config['smtp'].get('IMAP_PASSWORD', SMTP_PASSWORD)
|
||||
IMAP_FOLDER = config['smtp'].get('IMAP_FOLDER', 'inbox')
|
||||
|
||||
# repeater
|
||||
repeater_enabled = config['repeater'].getboolean('enabled', False)
|
||||
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
|
||||
|
||||
# scheduler
|
||||
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
|
||||
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
|
||||
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
|
||||
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
|
||||
schedulerInterval = config['scheduler'].get('interval', '') # default empty
|
||||
schedulerTime = config['scheduler'].get('time', '') # default empty
|
||||
schedulerValue = config['scheduler'].get('value', '') # default empty
|
||||
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
|
||||
@@ -167,9 +332,13 @@ try:
|
||||
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
|
||||
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
|
||||
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
|
||||
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
|
||||
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
|
||||
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
|
||||
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
|
||||
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
@@ -179,11 +348,16 @@ try:
|
||||
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
|
||||
mastermind_enabled = config['games'].getboolean('mastermind', True)
|
||||
golfSim_enabled = config['games'].getboolean('golfSim', True)
|
||||
hangman_enabled = config['games'].getboolean('hangman', True)
|
||||
hamtest_enabled = config['games'].getboolean('hamtest', True)
|
||||
|
||||
# messaging settings
|
||||
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
|
||||
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
266
modules/smtp.py
Normal file
266
modules/smtp.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# SMTP module for the meshing-around bot
|
||||
# 2024 Idea and code bits from https://github.com/tremmert81
|
||||
# https://avtech.com/articles/138/list-of-email-to-sms-addresses/
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
import pickle
|
||||
import time
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
# System variables
|
||||
trap_list_smtp = ("email:", "setemail", "sms:", "setsms", "clearsms")
|
||||
smtpThrottle = {}
|
||||
SMTP_TIMEOUT = 10
|
||||
|
||||
if enableImap:
|
||||
# Import IMAP library
|
||||
import imaplib
|
||||
import email
|
||||
|
||||
# Send email
|
||||
def send_email(to_email, message, nodeID=0):
|
||||
global smtpThrottle
|
||||
|
||||
# Clean up email address
|
||||
to_email = to_email.strip()
|
||||
|
||||
# Basic email validation
|
||||
if "@" not in to_email or "." not in to_email:
|
||||
logger.warning(f"System: Invalid email address format: {to_email}")
|
||||
return False
|
||||
|
||||
# throttle email to prevent abuse
|
||||
if to_email in smtpThrottle:
|
||||
if smtpThrottle[to_email] > time.time() - 120:
|
||||
logger.warning("System: Email throttled for " + to_email[:-6])
|
||||
return "⛔️Email throttled, try again later"
|
||||
smtpThrottle[to_email] = time.time()
|
||||
|
||||
# check if email is in the ban list
|
||||
if nodeID in bbs_ban_list:
|
||||
logger.warning("System: Email blocked for " + str(nodeID))
|
||||
return "⛔️Email throttled, try again later"
|
||||
# Send email
|
||||
try:
|
||||
# Create message
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = FROM_EMAIL
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = EMAIL_SUBJECT
|
||||
msg.attach(MIMEText(message, 'plain'))
|
||||
|
||||
# Connect to SMTP server
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=SMTP_TIMEOUT)
|
||||
try:
|
||||
# login /auth
|
||||
if SMTP_PORT == 587:
|
||||
server.starttls()
|
||||
if SMTP_AUTH:
|
||||
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Failed to login to SMTP server: {str(e)}")
|
||||
return
|
||||
|
||||
# Send email; this command will hold the program until the email is sent
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
logger.info("System: Email sent to: " + to_email[:-6])
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Failed to send email: {str(e)}")
|
||||
return False
|
||||
|
||||
def check_email(nodeID, sysop=False):
|
||||
if not enableImap:
|
||||
return
|
||||
|
||||
try:
|
||||
# Connect to IMAP server
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=SMTP_TIMEOUT)
|
||||
mail.login(IMAP_USERNAME, IMAP_PASSWORD)
|
||||
mail.select(IMAP_FOLDER)
|
||||
|
||||
# Search for new emails
|
||||
status, data = mail.search(None, 'UNSEEN')
|
||||
if status == 'OK':
|
||||
for num in data[0].split():
|
||||
status, data = mail.fetch(num, '(RFC822)')
|
||||
if status == 'OK':
|
||||
email_message = email.message_from_bytes(data[0][1])
|
||||
email_from = email_message['from']
|
||||
email_subject = email_message['subject']
|
||||
email_body = ""
|
||||
|
||||
if not sysop:
|
||||
# Check if email is whitelisted by particpant in the mesh
|
||||
for address in sms_db[nodeID]:
|
||||
if address in email_from:
|
||||
email_body = email_message.get_payload()
|
||||
logger.info("System: Email received from: " + email_from[:-6] + " for " + str(nodeID))
|
||||
return email_body.strip()
|
||||
else:
|
||||
# Check if email is from sysop
|
||||
for address in sysopEmails:
|
||||
if address in email_from:
|
||||
email_body = email_message.get_payload()
|
||||
logger.info("System: SysOp Email received from: " + email_from[:-6] + " for sysop")
|
||||
return email_body.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("System: Failed to check email: " + str(e))
|
||||
return False
|
||||
|
||||
# initalize email db
|
||||
email_db = {}
|
||||
try:
|
||||
with open('data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except:
|
||||
logger.warning("System: Email db not found, creating a new one")
|
||||
with open('data/email_db.pickle', 'wb') as f:
|
||||
pickle.dump(email_db, f)
|
||||
|
||||
def store_email(nodeID, email):
|
||||
global email_db
|
||||
|
||||
# if not in db, add it
|
||||
logger.debug("System: Setting E-Mail for " + str(nodeID))
|
||||
email_db[nodeID] = email
|
||||
|
||||
# save to a pickle for persistence, this is a simple db, be mindful of risk
|
||||
with open('data/email_db.pickle', 'wb') as f:
|
||||
pickle.dump(email_db, f)
|
||||
f.close()
|
||||
return True
|
||||
|
||||
|
||||
# initalize SMS db
|
||||
sms_db = [{'nodeID': 0, 'sms':[]}]
|
||||
try:
|
||||
with open('data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except:
|
||||
logger.warning("System: SMS db not found, creating a new one")
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
|
||||
def store_sms(nodeID, sms):
|
||||
global sms_db
|
||||
try:
|
||||
logger.debug("System: Setting SMS for " + str(nodeID))
|
||||
# if not in db, add it
|
||||
if nodeID not in sms_db:
|
||||
sms_db.append({'nodeID': nodeID, 'sms': sms})
|
||||
else:
|
||||
# if in db, update it
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
item['sms'].append(sms)
|
||||
|
||||
# save to a pickle for persistence, this is a simple db, be mindful of risk
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
f.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("System: Failed to store SMS: " + str(e))
|
||||
return False
|
||||
|
||||
def handle_sms(nodeID, message):
|
||||
global sms_db
|
||||
# if clearsms, remove all sms for node
|
||||
if message.lower().startswith("clearsms"):
|
||||
if any(item['nodeID'] == nodeID for item in sms_db):
|
||||
# remove record from db for nodeID
|
||||
sms_db = [item for item in sms_db if item['nodeID'] != nodeID]
|
||||
# update the pickle
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
f.close()
|
||||
return "📲 address cleared"
|
||||
return "📲No address to clear"
|
||||
|
||||
# send SMS to SMS in db. if none ask for one
|
||||
if message.lower().startswith("setsms"):
|
||||
message = message.split(" ", 1)
|
||||
if len(message[1]) < 5:
|
||||
return "?📲setsms: example@phone.co"
|
||||
if "@" not in message[1] and "." not in message[1]:
|
||||
return "📲Please provide a valid email address"
|
||||
if store_sms(nodeID, message[1]):
|
||||
return "📲SMS address set 📪"
|
||||
else:
|
||||
return "⛔️Failed to set address"
|
||||
|
||||
if message.lower().startswith("sms:"):
|
||||
message = message.split(" ", 1)
|
||||
if any(item['nodeID'] == nodeID for item in sms_db):
|
||||
count = 0
|
||||
# for all dict items maching nodeID in sms_db send sms
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
smsEmail = item['sms']
|
||||
logger.info("System: Sending SMS for " + str(nodeID) + " to " + smsEmail[:-6])
|
||||
if send_email(smsEmail, message[1], nodeID):
|
||||
count += 1
|
||||
else:
|
||||
return "⛔️Failed to send SMS"
|
||||
return "📲SMS sent " + str(count) + " addresses 📤"
|
||||
else:
|
||||
return "📲No address set, use 📲setsms"
|
||||
|
||||
return "Error: ⛔️ not understood. use:setsms example@phone.co"
|
||||
|
||||
def handle_email(nodeID, message):
|
||||
global email_db
|
||||
try:
|
||||
# send email to email in db. if none ask for one
|
||||
if message.lower().startswith("setemail"):
|
||||
message = message.split(" ", 1)
|
||||
if len(message) < 2:
|
||||
return "📧Please provide an email address"
|
||||
email_addr = message[1].strip()
|
||||
if "@" not in email_addr or "." not in email_addr:
|
||||
return "📧Please provide a valid email address"
|
||||
if store_email(nodeID, email_addr):
|
||||
return "📧Email address set 📪"
|
||||
return "Error: ⛔️ Failed to set email address"
|
||||
|
||||
if message.lower().startswith("email:"):
|
||||
parts = message.split(" ", 1)
|
||||
if len(parts) < 2:
|
||||
return "Error: ⛔️ format should be: email: message or, email: address@example.com #message"
|
||||
|
||||
content = parts[1].strip()
|
||||
|
||||
# Check if this is a direct email with address
|
||||
if "@" in content and "#" in content:
|
||||
# Split into email and message
|
||||
addr_msg = content.split("#", 1)
|
||||
if len(addr_msg) != 2:
|
||||
return "Error: ⛔️ Message format should be: email: address@example.com #message"
|
||||
|
||||
to_email = addr_msg[0].strip()
|
||||
message_body = addr_msg[1].strip()
|
||||
|
||||
logger.info(f"System: Sending email for {nodeID} to {to_email}")
|
||||
if send_email(to_email, message_body, nodeID):
|
||||
return "📧Email-sent 📤"
|
||||
return "⛔️Failed to send email"
|
||||
|
||||
# Using stored email address
|
||||
elif nodeID in email_db:
|
||||
logger.info(f"System: Sending email for {nodeID} to stored address")
|
||||
if send_email(email_db[nodeID], content, nodeID):
|
||||
return "📧Email-sent 📤"
|
||||
return "⛔️Failed to send email"
|
||||
|
||||
return "Error: ⛔️ no email on file. use: setemail"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Email handling error: {str(e)}")
|
||||
return "⛔️Failed to process email command"
|
||||
@@ -9,7 +9,7 @@ import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond")
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
|
||||
|
||||
def hf_band_conditions():
|
||||
# ham radio HF band conditions
|
||||
@@ -140,3 +140,43 @@ def get_moon(lat=0, lon=0):
|
||||
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
|
||||
|
||||
return moon_data
|
||||
|
||||
def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
pass_data = ''
|
||||
# get the next satellite pass for a given satellite
|
||||
visualPassAPI = "https://api.n2yo.com/rest/v1/satellite/visualpasses/"
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
# API URL
|
||||
if n2yoAPIKey == '':
|
||||
logger.error("System: Missing API key free at https://www.n2yo.com/login/")
|
||||
return "not configured, bug your sysop"
|
||||
url = visualPassAPI + str(satellite) + "/" + str(lat) + "/" + str(lon) + "/0/2/300/" + "&apiKey=" + n2yoAPIKey
|
||||
# get the next pass data
|
||||
try:
|
||||
if not int(satellite):
|
||||
raise Exception("Invalid satellite number")
|
||||
next_pass_data = requests.get(url, timeout=urlTimeoutSeconds)
|
||||
if(next_pass_data.ok):
|
||||
pass_json = next_pass_data.json()
|
||||
if 'info' in pass_json and 'passescount' in pass_json['info'] and pass_json['info']['passescount'] > 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_time = pass_json['passes'][0]['startUTC']
|
||||
pass_duration = pass_json['passes'][0]['duration']
|
||||
pass_maxEl = pass_json['passes'][0]['maxEl']
|
||||
pass_rise_time = datetime.fromtimestamp(pass_time).strftime('%a %d %I:%M%p')
|
||||
pass_startAzCompass = pass_json['passes'][0]['startAzCompass']
|
||||
pass_set_time = datetime.fromtimestamp(pass_time + pass_duration).strftime('%a %d %I:%M%p')
|
||||
pass__endAzCompass = pass_json['passes'][0]['endAzCompass']
|
||||
pass_data = f"{satname} @{pass_rise_time} Az:{pass_startAzCompass} for{getPrettyTime(pass_duration)}, MaxEl:{pass_maxEl}° Set@{pass_set_time} Az:{pass__endAzCompass}"
|
||||
elif pass_json['info']['passescount'] == 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_data = f"{satname} has no upcoming passes"
|
||||
else:
|
||||
logger.error(f"System: Error fetching satellite pass data {satellite}")
|
||||
pass_data = ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.warning(f"System: User supplied value {satellite} unknown or invalid")
|
||||
pass_data = "Provide NORAD# example use:🛰️satpass 25544,33591"
|
||||
return pass_data
|
||||
File diff suppressed because it is too large
Load Diff
62
modules/web.py
Normal file
62
modules/web.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# This is a simple web server that serves up the content of the webRoot directory
|
||||
# The reporting data is all that is currently being served up
|
||||
# TODO - add interaction to mesh?
|
||||
# to use this today run it seperately and open a browser to http://localhost:8420
|
||||
|
||||
import os
|
||||
import http.server
|
||||
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
|
||||
# Set the port for the server
|
||||
PORT = 8420
|
||||
|
||||
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
|
||||
SSL = False
|
||||
|
||||
# Set to True to enable logging sdtout
|
||||
webServerLogs = False
|
||||
|
||||
# Determine the directory where this script is located.
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# Go up one level from the modules directory to the project root.
|
||||
project_root = os.path.abspath(os.path.join(script_dir, ".."))
|
||||
|
||||
# Build the absolute path to the webRoot folder; to where index.html is located.
|
||||
webRoot = os.path.join(project_root, "etc", "www")
|
||||
|
||||
if SSL:
|
||||
import ssl
|
||||
|
||||
# disable logging
|
||||
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
if webServerLogs:
|
||||
super().log_message(format, *args)
|
||||
|
||||
# Change the current working directory to webRoot
|
||||
os.chdir(webRoot)
|
||||
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
if SSL:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
try:
|
||||
ctx.load_cert_chain(certfile='./server.pem')
|
||||
except FileNotFoundError:
|
||||
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
|
||||
exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
print(f"Serving reports at https://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
else:
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
|
||||
if not webServerLogs:
|
||||
print("Server Logs are disabled")
|
||||
# Serve forever, that is until the user interrupts the process
|
||||
httpd.serve_forever()
|
||||
exit(0)
|
||||
@@ -1,18 +1,19 @@
|
||||
import openmeteo_requests # pip install openmeteo-requests
|
||||
from retry_requests import retry # pip install retry_requests
|
||||
#import requests_cache
|
||||
#import openmeteo_requests # pip install openmeteo-requests
|
||||
#from retry_requests import retry # pip install retry_requests
|
||||
|
||||
import requests
|
||||
import json
|
||||
from modules.log import *
|
||||
|
||||
def get_weather_data(api_url, params):
|
||||
response = requests.get(api_url, params=params)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
return response.json()
|
||||
|
||||
def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Setup the Open-Meteo API client with cache and retry on error
|
||||
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||
retry_session = retry(retries = 3, backoff_factor = 0.2)
|
||||
openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||
|
||||
# Make sure all required weather variables are listed here
|
||||
# The order of variables in hourly or daily is important to assign them correctly below
|
||||
url = "https://api.open-meteo.com/v1/forecast"
|
||||
@@ -34,27 +35,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
try:
|
||||
# Fetch the weather data
|
||||
responses = openmeteo.weather_api(url, params=params)
|
||||
weather_data = get_weather_data(url, params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
response = responses[0]
|
||||
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
|
||||
|
||||
# Process location
|
||||
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = weather_data
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response.Daily()
|
||||
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
|
||||
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
|
||||
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
|
||||
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
|
||||
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
|
||||
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
|
||||
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
|
||||
daily = response['daily']
|
||||
daily_weather_code = daily['weather_code']
|
||||
daily_temperature_2m_max = daily['temperature_2m_max']
|
||||
daily_temperature_2m_min = daily['temperature_2m_min']
|
||||
daily_precipitation_hours = daily['precipitation_hours']
|
||||
daily_precipitation_probability_max = daily['precipitation_probability_max']
|
||||
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
|
||||
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
|
||||
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
@@ -191,3 +194,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather_report
|
||||
|
||||
def get_flood_openmeteo(lat=0, lon=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Flood data
|
||||
url = "https://flood-api.open-meteo.com/v1/flood"
|
||||
params = {
|
||||
"latitude": {lat},
|
||||
"longitude": {lon},
|
||||
"timezone": "auto",
|
||||
"daily": "river_discharge",
|
||||
"forecast_days": forecastDays
|
||||
}
|
||||
|
||||
try:
|
||||
# Fetch the flood data
|
||||
flood_data = get_weather_data(url, params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = flood_data
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response['daily']
|
||||
daily_river_discharge = daily['river_discharge']
|
||||
# check if none
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# create a flood report
|
||||
flood_report = ""
|
||||
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
|
||||
|
||||
return flood_report
|
||||
|
||||
427
pong_bot.py
427
pong_bot.py
@@ -2,30 +2,41 @@
|
||||
# Meshtastic Autoresponder PONG Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
except ImportError:
|
||||
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh pong' using a virtual environment.")
|
||||
exit(1)
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from pubsub import pub # pip install pubsub
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# Global Variables
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
# Auto response to messages
|
||||
message_lower = message.lower()
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
"ping": lambda: handle_ping(message, hop, snr, rssi),
|
||||
"pong": lambda: "🏓Ping!!",
|
||||
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"ack": lambda: handle_ack(hop, snr, rssi),
|
||||
"testing": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_testing(hop, snr, rssi),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
}
|
||||
cmds = [] # list to hold the commands found in the message
|
||||
for key in command_handler:
|
||||
@@ -44,17 +55,96 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_ping(message, hop, snr, rssi):
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
def handle_cmd(message, message_from_id, deviceID):
|
||||
# why CMD? its just a command list. a terminal would normally use "Help"
|
||||
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
|
||||
|
||||
msg = ""
|
||||
type = ''
|
||||
|
||||
if "ping" in message.lower():
|
||||
msg = "🏓PONG\n"
|
||||
type = "🏓PING"
|
||||
elif "test" in message.lower() or "testing" in message.lower():
|
||||
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
|
||||
"🎙Testing, testing\n",\
|
||||
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
|
||||
"🎙Roger that!\n",])
|
||||
type = "🎙TEST"
|
||||
elif "ack" in message.lower():
|
||||
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
|
||||
type = "✋ACK"
|
||||
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
|
||||
if deviceID == 1:
|
||||
myname = get_name_from_number(myNodeNum1, 'short', 1)
|
||||
elif deviceID == 2:
|
||||
myname = get_name_from_number(myNodeNum2, 'short', 2)
|
||||
msg = f"QSP QSL OM DE {myname} K\n"
|
||||
else:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
msg = msg + hop
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
type = type + " @" + message.split("@")[1]
|
||||
elif "#" in message:
|
||||
msg = msg + " #" + message.split("#")[1]
|
||||
type = type + " #" + message.split("#")[1]
|
||||
|
||||
|
||||
# check for multi ping request
|
||||
if " " in message:
|
||||
# if stop multi ping
|
||||
if "stop" in message.lower():
|
||||
for i in range(0, len(multiPingList)):
|
||||
if multiPingList[i].get('message_from_id') == message_from_id:
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
return "🏓PONG, " + hop
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
|
||||
def handle_motd(message):
|
||||
global MOTD
|
||||
@@ -64,65 +154,47 @@ def handle_motd(message):
|
||||
return "MOTD Set to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
|
||||
|
||||
# display last heard nodes add to response
|
||||
bot_response = "Last Heard\n"
|
||||
bot_response += str(get_node_list(1))
|
||||
|
||||
# show last users of the bot with the cmdHistory list
|
||||
history = handle_history(message, nodeid, deviceID, isDM, lheard=True)
|
||||
if history:
|
||||
bot_response += f'LastSeen\n{history}'
|
||||
else:
|
||||
# trim the last \n
|
||||
bot_response = bot_response[:-1]
|
||||
|
||||
# bot_response += getNodeTelemetry(deviceID)
|
||||
return bot_response
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "✋ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🎙Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
global seenNodes
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
# extract interface details from inbound packet
|
||||
rxType = type(interface).__name__
|
||||
rxNode = 0
|
||||
message_from_id = 0
|
||||
snr = 0
|
||||
rssi = 0
|
||||
hop = 0
|
||||
hop_away = 0
|
||||
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
isDM = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
for item in interface.__dict__.items(): intDebug = f"{item}\n"
|
||||
@@ -130,62 +202,93 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface:
|
||||
rxNode = 1
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
rxNode = 2
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# check for a message packet and process it
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# handle TEXT_MESSAGE_APP
|
||||
try:
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
try:
|
||||
snr = packet['rxSnr']
|
||||
rssi = packet['rxRssi']
|
||||
except KeyError:
|
||||
snr = 0
|
||||
rssi = 0
|
||||
|
||||
if packet.get('channel'):
|
||||
channel_number = packet['channel']
|
||||
else:
|
||||
channel_number = publicChannel
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
snr = packet.get('rxSnr', 0)
|
||||
rssi = packet.get('rxRssi', 0)
|
||||
|
||||
# check if the packet has a publicKey flag use it
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet['hopsAway']
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
hop_away = 0
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet['hopLimit']
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet['hopStart']
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
@@ -196,46 +299,53 @@ def onReceive(packet, interface):
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
|
||||
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
return
|
||||
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
|
||||
# message is DM to us
|
||||
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if messageTrap(message_string):
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# log the message to the message log
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
else:
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
|
||||
|
||||
else:
|
||||
# message is not for bot to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
@@ -257,45 +367,63 @@ def onReceive(packet, interface):
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
if repeater_enabled and multiple_interface:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
if rxNode == 1:
|
||||
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 2)
|
||||
elif rxNode == 2:
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
|
||||
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
print(packet) # print the packet for debugging
|
||||
print("END of packet \n")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
async def start_rx():
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False):
|
||||
myNodeNum = globals().get(f'myNodeNum{i}', 0)
|
||||
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
|
||||
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
|
||||
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
logger.debug("System: Logging System Logs to disk")
|
||||
if solar_conditions_enabled:
|
||||
logger.debug("System: Celestial Telemetry Enabled")
|
||||
if motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if sentry_enabled:
|
||||
logger.debug("System: Sentry Enabled")
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug("System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and multiple_interface:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
if file_monitor_enabled:
|
||||
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
|
||||
if read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
@@ -306,14 +434,19 @@ async def start_rx():
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
await asyncio.wait([meshRxTask, watchdogTask])
|
||||
if file_monitor_enabled:
|
||||
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
if file_monitor_enabled:
|
||||
await asyncio.gather(fileMonTask)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
|
||||
# EOF
|
||||
|
||||
@@ -3,15 +3,10 @@ pubsub
|
||||
datetime
|
||||
pyephem
|
||||
requests
|
||||
geopy
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
ollama
|
||||
googlesearch-python
|
||||
|
||||
25
script/docker/README.md
Normal file
25
script/docker/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# How do I use this thing?
|
||||
This is not a full turnkey setup for Docker yet but gets you most of the way there!
|
||||
|
||||
## Setup New Image
|
||||
`docker build -t meshing-around .`
|
||||
|
||||
there is also [script/docker/docker-install.bat](script/docker/docker-install.bat) which will automate this.
|
||||
|
||||
## Ollama Image with compose
|
||||
still a WIP
|
||||
`docker compose up -d`
|
||||
|
||||
## Edit the config.ini in the docker
|
||||
To edit the config.ini in the docker you can
|
||||
`docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"`
|
||||
|
||||
there is also [script/docker/docker-terminal.bat](script/docker/docker-terminal.bat) which will open nano to edit.
|
||||
ctl+o to write out and exit editor in shell
|
||||
|
||||
## other info
|
||||
1. Ensure your serial port is properly shared.
|
||||
2. Run the Docker container:
|
||||
```sh
|
||||
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
|
||||
```
|
||||
52
script/docker/compose.yaml
Normal file
52
script/docker/compose.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
meshing-around:
|
||||
build:
|
||||
context: ../..
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
devices:
|
||||
- /dev/ttyAMA10 # Replace this with your actual device!
|
||||
configs:
|
||||
- source: me_config
|
||||
target: /app/config.ini
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp
|
||||
ollama:
|
||||
image: ollama/ollama:0.5.1
|
||||
volumes:
|
||||
- ./ollama:/root/.ollama
|
||||
- ./ollama-entrypoint.sh:./entrypoint.sh
|
||||
container_name: ollama
|
||||
pull_policy: always
|
||||
tty: true
|
||||
restart: always
|
||||
entrypoint:
|
||||
- /usr/bin/bash
|
||||
- /script/docker/entrypoint.sh
|
||||
expose:
|
||||
- 11434
|
||||
healthcheck:
|
||||
test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
node-exporter:
|
||||
image: quay.io/prometheus/node-exporter:latest
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.rootfs=/rootfs
|
||||
- --path.sysfs=/host/sys
|
||||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9100
|
||||
network_mode: host
|
||||
pid: host
|
||||
configs:
|
||||
me_config:
|
||||
file: ./config.ini
|
||||
6
script/docker/docker-install.bat
Normal file
6
script/docker/docker-install.bat
Normal file
@@ -0,0 +1,6 @@
|
||||
REM batch file to install docker on windows
|
||||
REM docker compose up -d
|
||||
cd ../../
|
||||
docker build -t meshing-around .
|
||||
REM docker-compose up -d
|
||||
docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"
|
||||
2
script/docker/docker-terminal.bat
Normal file
2
script/docker/docker-terminal.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
REM launch meshing-around container with a terminal
|
||||
docker run -it --entrypoint /bin/bash meshing-around
|
||||
6
script/docker/entrypoint.sh
Normal file
6
script/docker/entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# instruction set the meshing-around docker container entrypoint
|
||||
# Substitute environment variables in the config file (what is the purpose of this?)
|
||||
# envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
# Run the bot
|
||||
exec python /app/mesh_bot.py
|
||||
16
script/docker/ollama-entrypoint.sh
Normal file
16
script/docker/ollama-entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start Ollama in the background.
|
||||
/bin/ollama serve &
|
||||
# Record Process ID.
|
||||
pid=$!
|
||||
|
||||
# Pause for Ollama to start.
|
||||
sleep 5
|
||||
|
||||
echo "🔴 Retrieve llama3.2:3b model..."
|
||||
ollama pull llama3.2:3b
|
||||
echo "🟢 Done!"
|
||||
|
||||
# Wait for Ollama process to finish.
|
||||
wait $pid
|
||||
7
script/runShell.sh
Normal file
7
script/runShell.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# meshing-around demo script for shell scripting
|
||||
# runShell.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
|
||||
printf "Running meshing-around demo script for shell scripting from $program_path\n"
|
||||
53
script/send-environment-metrics.py
Normal file
53
script/send-environment-metrics.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# file name: send-environment-metrics.py
|
||||
# https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/send-environment-metrics.py
|
||||
|
||||
from meshtastic.protobuf import portnums_pb2, telemetry_pb2
|
||||
from meshtastic import BROADCAST_ADDR
|
||||
import time
|
||||
|
||||
# For connection over serial
|
||||
# import meshtastic.serial_interface
|
||||
# interface = meshtastic.serial_interface.SerialInterface()
|
||||
|
||||
# For connection over TCP
|
||||
import meshtastic.tcp_interface
|
||||
interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=False)
|
||||
|
||||
# Create a telemetry data object
|
||||
telemetry_data = telemetry_pb2.Telemetry()
|
||||
telemetry_data.time = int(time.time())
|
||||
#telemetry_data.local_stats.upTime = 0
|
||||
telemetry_data.environment_metrics.temperature = 0
|
||||
# telemetry_data.environment_metrics.voltage = 0
|
||||
# telemetry_data.environment_metrics.current = 0
|
||||
# telemetry_data.environment_metrics.relative_humidity = 0
|
||||
# telemetry_data.environment_metrics.barometric_pressure = 0
|
||||
# telemetry_data.environment_metrics.gas_resistance = 0
|
||||
# telemetry_data.environment_metrics.iaq = 0
|
||||
# telemetry_data.environment_metrics.distance = 0
|
||||
# telemetry_data.environment_metrics.lux = 0
|
||||
# telemetry_data.environment_metrics.white_lux = 0
|
||||
# telemetry_data.environment_metrics.ir_lux = 0
|
||||
# telemetry_data.environment_metrics.uv_lux = 0
|
||||
# telemetry_data.environment_metrics.wind_direction = 0
|
||||
# telemetry_data.environment_metrics.wind_speed = 0
|
||||
# telemetry_data.environment_metrics.wind_gust = 0
|
||||
# telemetry_data.environment_metrics.wind_lull = 0
|
||||
# telemetry_data.environment_metrics.weight = 0
|
||||
|
||||
# Read the uptime
|
||||
# with open('/proc/uptime', 'r') as uptime:
|
||||
# telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
|
||||
|
||||
# Read the CPU temperature
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
|
||||
telemetry_data.environment_metrics.temperature = int(cpu_temp.read()) / 1000
|
||||
|
||||
interface.sendData(
|
||||
telemetry_data,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
|
||||
wantResponse=False,
|
||||
)
|
||||
|
||||
interface.close()
|
||||
27
script/sysEnv.sh
Normal file
27
script/sysEnv.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# meshing-around shell script for sysinfo
|
||||
# runShell.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
|
||||
# get basic telemetry data. Free space, CPU, RAM, and temperature for a raspberry pi
|
||||
free_space=$(df -h | grep ' /$' | awk '{print $4}')
|
||||
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
|
||||
ram_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}')
|
||||
ram_free=$(echo "scale=2; 100 - $ram_usage" | bc)
|
||||
|
||||
# if command vcgencmd is found, part of raspberrypi tools, use it to get temperature
|
||||
if command -v vcgencmd &> /dev/null
|
||||
then
|
||||
# get temperature
|
||||
temp=$(vcgencmd measure_temp | sed "s/temp=//" | sed "s/'C//")
|
||||
# temp in fahrenheit
|
||||
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
|
||||
else
|
||||
# get temperature from thermal zone
|
||||
temp=$(paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | grep "temp" | awk '{print $2/1000}' | awk '{s+=$1} END {print s/NR}')
|
||||
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
|
||||
fi
|
||||
|
||||
# print telemetry data rounded to 2 decimal places
|
||||
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
|
||||
Reference in New Issue
Block a user