mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
451 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00280e351c | ||
|
|
0e8bb197a9 | ||
|
|
d825c0fa15 | ||
|
|
6abe73c1bc | ||
|
|
b8e9adb223 | ||
|
|
e621016e9a | ||
|
|
cfaf652852 | ||
|
|
6c27b5d5de | ||
|
|
a31fa90942 | ||
|
|
3cd347dff3 | ||
|
|
ea4ac1f9c1 | ||
|
|
a9da8336cc | ||
|
|
4ba60ed276 | ||
|
|
42e07d44e6 | ||
|
|
11f5218c2e | ||
|
|
e137420138 | ||
|
|
80c0f698b6 | ||
|
|
2045bf98f7 | ||
|
|
c36ce2c3a6 | ||
|
|
7ff36a3d5f | ||
|
|
ae1a3040b5 | ||
|
|
84b6b48d60 | ||
|
|
9f3446b605 | ||
|
|
10add3147d | ||
|
|
4e074a309f | ||
|
|
f394f58b9f | ||
|
|
30bcee498d | ||
|
|
5c54ce0b70 | ||
|
|
b4684f49ff | ||
|
|
c6c1e9f637 | ||
|
|
54540b1656 | ||
|
|
006c9f58c6 | ||
|
|
3d582e9b77 | ||
|
|
0578c0b233 | ||
|
|
98ccf8708f | ||
|
|
47280f4330 | ||
|
|
4b0b074ba7 | ||
|
|
d8d79f46b5 | ||
|
|
1a49a81cf5 | ||
|
|
5c0b04f0b7 | ||
|
|
8f1cb6265d | ||
|
|
c51a4584ae | ||
|
|
13ee6d4fd6 | ||
|
|
2d44faac98 | ||
|
|
0a2daeac1f | ||
|
|
da7ba256d8 | ||
|
|
42e99a0dc1 | ||
|
|
ca5896a015 | ||
|
|
ebe2636104 | ||
|
|
971a421d01 | ||
|
|
9d080de8f3 | ||
|
|
5209092928 | ||
|
|
6439f49fb1 | ||
|
|
c115cdf82f | ||
|
|
63fccbdf3e | ||
|
|
c23564d8b5 | ||
|
|
0b9db28951 | ||
|
|
1aa4eddb3b | ||
|
|
a7de64b385 | ||
|
|
2e8206d4ec | ||
|
|
b47c13503b | ||
|
|
a66dbd13fd | ||
|
|
02322cdf91 | ||
|
|
955d3681e9 | ||
|
|
9d96c02870 | ||
|
|
c87dba1e06 | ||
|
|
1c3d2f7f18 | ||
|
|
b53a7d3832 | ||
|
|
99e74ae8c0 | ||
|
|
1bdfc3828f | ||
|
|
26f39e76e6 | ||
|
|
c49dcfbfc8 | ||
|
|
1008ec6afa | ||
|
|
8c0a1bbd0d | ||
|
|
30d8f00aeb | ||
|
|
033b1bcd51 | ||
|
|
d80b2da06a | ||
|
|
dc02464662 | ||
|
|
b738881ff1 | ||
|
|
b8f0601684 | ||
|
|
910c045b08 | ||
|
|
35ba139577 | ||
|
|
eddd990cc5 | ||
|
|
6e3d83401f | ||
|
|
f9ab6a79d3 | ||
|
|
4b2402c286 | ||
|
|
47e0276f0c | ||
|
|
6dc54abf43 | ||
|
|
53ff37c782 | ||
|
|
5c48e008ee | ||
|
|
ddac18bb13 | ||
|
|
507919bb4c | ||
|
|
b4f3f9887d | ||
|
|
aa051abbd4 | ||
|
|
8ba0c6f14c | ||
|
|
166b15463a | ||
|
|
d02924bfda | ||
|
|
0a123251f4 | ||
|
|
a80f926d08 | ||
|
|
b8318f8f3e | ||
|
|
adb6fa3b5a | ||
|
|
a9254c9c79 | ||
|
|
0ac642ac44 | ||
|
|
ec24a8b8dd | ||
|
|
f79026a95f | ||
|
|
99acaf28a1 | ||
|
|
9c068c8d28 | ||
|
|
d9ab1b88c1 | ||
|
|
8499b6c851 | ||
|
|
ea47bf9329 | ||
|
|
229043c32a | ||
|
|
5dbd137f14 | ||
|
|
ca83117180 | ||
|
|
fde37313f5 | ||
|
|
a39f588f9f | ||
|
|
39e348f701 | ||
|
|
4fdfa49b87 | ||
|
|
8a64b8e7ad | ||
|
|
c585f60882 | ||
|
|
4e91801cb9 | ||
|
|
4e1d3e2b58 | ||
|
|
38d5006236 | ||
|
|
642738e3b6 | ||
|
|
5728d6b9e3 | ||
|
|
82bec43f22 | ||
|
|
7ca8b6793a | ||
|
|
d589d3e155 | ||
|
|
452c4aa520 | ||
|
|
e5d2ea4bcb | ||
|
|
2596d133fd | ||
|
|
c3221d64a8 | ||
|
|
dcabfc0f50 | ||
|
|
2eca5f644a | ||
|
|
3f90a7fc39 | ||
|
|
9639c793d9 | ||
|
|
18294f4ca3 | ||
|
|
14043dd950 | ||
|
|
fe1c264b19 | ||
|
|
8848d9b6fe | ||
|
|
daa6e85318 | ||
|
|
f7f127590d | ||
|
|
5b0fd65d31 | ||
|
|
fe12e1f107 | ||
|
|
11025f101f | ||
|
|
92a5fc2ed5 | ||
|
|
3f891d93d2 | ||
|
|
398c9ddb60 | ||
|
|
518eb25f0d | ||
|
|
0613cc7b3d | ||
|
|
be2d1fbf72 | ||
|
|
8595190ed1 | ||
|
|
5703cfb381 | ||
|
|
ff9b76c966 | ||
|
|
e41f692038 | ||
|
|
19935d9f08 | ||
|
|
f0e8b2c057 | ||
|
|
eb2d809fe4 | ||
|
|
b276bbb40a | ||
|
|
35ea7cb505 | ||
|
|
eb78c2e5e8 | ||
|
|
3df16b7626 | ||
|
|
8c01433d14 | ||
|
|
a8dbef7e12 | ||
|
|
23478812e0 | ||
|
|
08c2c668f9 | ||
|
|
3b41e39ff5 | ||
|
|
78fa3209e6 | ||
|
|
c5ba56a656 | ||
|
|
50c3249edc | ||
|
|
80897f7a82 | ||
|
|
d311832a92 | ||
|
|
56af59345d | ||
|
|
c1adca7db0 | ||
|
|
4c7fe55b43 | ||
|
|
df6a1cfb66 | ||
|
|
9994446510 | ||
|
|
9272218815 | ||
|
|
388d862fc9 | ||
|
|
ac33f8a02b | ||
|
|
f04392a81c | ||
|
|
d0097c092b | ||
|
|
92ff166260 | ||
|
|
bfe0d219f9 | ||
|
|
85a2d90cff | ||
|
|
e15232875c | ||
|
|
d1a87f161b | ||
|
|
626ac59b4e | ||
|
|
835a9e5f89 | ||
|
|
3ae928dd66 | ||
|
|
3973406783 | ||
|
|
4fbdd42837 | ||
|
|
04378efdd8 | ||
|
|
0d19a40ed6 | ||
|
|
75ac3c974a | ||
|
|
7e0eb348ae | ||
|
|
af6ea2a512 | ||
|
|
6665ea7dcd | ||
|
|
3212661ee8 | ||
|
|
0675132171 | ||
|
|
fdb7897963 | ||
|
|
8ff7a0bf3c | ||
|
|
c210534543 | ||
|
|
ea7574a868 | ||
|
|
8f69c4d93c | ||
|
|
bc9ada91b4 | ||
|
|
28f06f0a21 | ||
|
|
267fe392e3 | ||
|
|
6c1f7940ca | ||
|
|
2fc9281394 | ||
|
|
b5bd1008c2 | ||
|
|
ee1db5b7be | ||
|
|
7395b96337 | ||
|
|
f3c6f77b23 | ||
|
|
f6e04a42a0 | ||
|
|
3fcd588d02 | ||
|
|
e1b47484f2 | ||
|
|
14798cb992 | ||
|
|
41c8f0044b | ||
|
|
45eefb24d8 | ||
|
|
410d32947c | ||
|
|
748652ac62 | ||
|
|
d715cb6b4d | ||
|
|
1895a365ae | ||
|
|
cc58a38165 | ||
|
|
a8ccb05d56 | ||
|
|
a90a533a30 | ||
|
|
57a4e5d68c | ||
|
|
7c99b684ad | ||
|
|
b957c89d70 | ||
|
|
9b986dd57a | ||
|
|
9e348332e5 | ||
|
|
0cfe759ef6 | ||
|
|
e95902ef98 | ||
|
|
c7df4d88d1 | ||
|
|
6d01c5a986 | ||
|
|
3f882dcfcd | ||
|
|
b146fd6f64 | ||
|
|
8709e5aed5 | ||
|
|
caf8a2708b | ||
|
|
9b4200c198 | ||
|
|
097cae6e94 | ||
|
|
0a260b28b6 | ||
|
|
3f5c6f2e9a | ||
|
|
8a4f7a904a | ||
|
|
0bc3d392cf | ||
|
|
5eaef8b5b8 | ||
|
|
3a0007771d | ||
|
|
67ba2b1fb5 | ||
|
|
f2e7a9aa5c | ||
|
|
9d22270dde | ||
|
|
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 | ||
|
|
5079c67f62 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -8,7 +8,8 @@ config.ini
|
||||
venv/
|
||||
|
||||
# logs
|
||||
logs/*.log
|
||||
logs/
|
||||
install_notes.txt
|
||||
|
||||
# modified .service files
|
||||
etc/*.service
|
||||
@@ -18,3 +19,11 @@ __pycache__/
|
||||
|
||||
# rag data
|
||||
data/rag/*
|
||||
|
||||
# qrz db
|
||||
data/qrz.db
|
||||
|
||||
# fileMon
|
||||
news.txt
|
||||
alert.txt
|
||||
bee.txt
|
||||
|
||||
@@ -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
|
||||
314
README.md
314
README.md
@@ -11,13 +11,15 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **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
|
||||
|
||||
### 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
|
||||
- **Network Monitoring**: Alert on noisy nodes, node locations, and best placment for relay nodes.
|
||||
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
### 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
|
||||
@@ -26,19 +28,26 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **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.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
|
||||
- **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), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **NOAA/USGS location Data**: Get localized weather(alerts), Earthquake, 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.
|
||||
- **Satellite Pass Info**: Get passes for satellite at your location.
|
||||
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
|
||||
|
||||
### 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. Useful foraccountability 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
|
||||
@@ -48,12 +57,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
### 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.
|
||||
- **UK.GOV Alerts**: Pulling data form the UK.GOV alert page
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
### File Monitor Alerts
|
||||
- **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.
|
||||
- **Shell Command Access**: Pass commands via DM directly to the host OS
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
@@ -62,25 +73,103 @@ 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. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), there is also [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
|
||||
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) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project. 🥔 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.
|
||||
|
||||
### Quick Setup
|
||||
#### 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!
|
||||
|
||||
#### Automation of 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](script/docker/README.md)
|
||||
## Full list of commands for the bot
|
||||
|
||||
#### Custom Install
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) | ✅ |
|
||||
| `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) via DM only | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
| `echo` | Echo string back, disabled by default | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forecasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
|
||||
| `earthquake` | Returns the largest and number of USGS events for the location | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `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` | Return local weather forecast, NOAA or Open Meteo (which also has `wxc` for metric and imperial) | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `mwx` | Return the NOAA Coastal Marine Forecast data | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `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) | ✅ |
|
||||
| `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 communications | |
|
||||
| `setsms` | Adds the SMS-Email for quick communications | |
|
||||
| `clearsms` | Clears all SMS-Emails on file for node | |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard on device, like Store and Forward, returns the PublicChannel and Current | ✅ |
|
||||
| `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` |
|
||||
| `howfar` | returns the distance you have traveled since your last HowFar. `howfar reset` to start over | ✅ |
|
||||
| `howtall` | returns height of something you give a shadow by using sun angle | ✅ |
|
||||
|
||||
### 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 only)
|
||||
| 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 | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
## Other Install Options
|
||||
|
||||
### Docker Installation - handy for windows
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -91,8 +180,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
|
||||
```
|
||||
@@ -125,6 +216,9 @@ The following settings determine how the bot responds. By default, the bot will
|
||||
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
|
||||
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
@@ -136,7 +230,12 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
|
||||
|
||||
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
|
||||
coastalForecastDays = 3 # number of data points to return, default is 3
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
@@ -170,6 +269,9 @@ 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
|
||||
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
@@ -178,8 +280,8 @@ To enable connectivity with SMTP allows messages from meshtastic into SMTP. The
|
||||
```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
|
||||
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
|
||||
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
|
||||
sysopEmails =
|
||||
# See config.template for all the SMTP settings
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
@@ -200,27 +302,47 @@ 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 UK.gov
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. UK.gov for England
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, 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
|
||||
ignoreFEMAtest = True # Ignore any headline that includes the word Test
|
||||
# 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
|
||||
enableGBalerts = False # use UK.gov for alert source
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
|
||||
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
#### USGS River flow data and Volcano alerts
|
||||
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
|
||||
|
||||
Volcano Alerts use lat/long to determine ~1000km radius
|
||||
```ini
|
||||
[location]
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverList = 14144700 # example Mouth of Columbia River
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
@@ -233,12 +355,12 @@ repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### 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) works on a pi58GB with 40 second or less response time.
|
||||
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 `gemma3:270m`. 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
|
||||
ollama = True # Ollama model to use (defaults to gemma2:2b)
|
||||
ollamaModel = gemma2 #ollamaModel = llama3.1
|
||||
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
|
||||
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
|
||||
```
|
||||
|
||||
@@ -246,6 +368,9 @@ Also see `llm.py` for changing the defaults of:
|
||||
|
||||
```ini
|
||||
# LLM System Variables
|
||||
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
|
||||
|
||||
# Used in the meshBotAI template (legacy)
|
||||
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
|
||||
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
|
||||
@@ -272,12 +397,15 @@ Some dev notes for ideas of use
|
||||
```ini
|
||||
[fileMon]
|
||||
filemon_enabled = True
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
file_path = alert.txt # text file to monitor for changes
|
||||
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
|
||||
enable_read_news = False # news command will return the contents of a text file
|
||||
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
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
|
||||
allowXcmd = True
|
||||
```
|
||||
|
||||
#### Offline EAS
|
||||
@@ -305,16 +433,28 @@ rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas
|
||||
#### 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
|
||||
|
||||
you can also enable the line by line (hint just search for the commented lines with a 🐝) to return a string from the [bee movie](https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt) for example adding it alongside news.txt as bee.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
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = True
|
||||
enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
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
|
||||
@@ -325,7 +465,7 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
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.
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, 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))
|
||||
@@ -335,76 +475,23 @@ bbslink_enabled = True
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
|
||||
### Firmware 2.6 DM Key, and 2.7 CLIENT_BASE Favorite Nodes
|
||||
Firmware 2.6 introduced [PKC](https://meshtastic.org/blog/introducing-new-public-key-cryptography-in-v2_5/), enabling secure private messaging by adding necessary keys to each node. To fully utilize this feature, you should add favorite nodes—such as BBS admins—to your node’s favorites list to ensure their keys are retained. A helper script is provided to simplify this process:
|
||||
- Run the helper script from the main program directory: `python3 script/addFav.py`
|
||||
- By default, this script adds nodes from `bbs_admin_list` and `bbslink_whitelist`
|
||||
- If using a virtual environment, run: `launch.sh addfav`
|
||||
|
||||
To configure favorite nodes, add their numbers to your config file:
|
||||
```conf
|
||||
[general]
|
||||
favoriteNodeList = # list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
|
||||
```
|
||||
|
||||
### 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. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
|
||||
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.6.11 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
|
||||
|
||||
~~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` | 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 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `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) |
|
||||
| `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 | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `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) | ✅ |
|
||||
| `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 | |
|
||||
|---------|-------------|-
|
||||
| `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` |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
# Recognition
|
||||
|
||||
I used ideas and snippets from other responder bots and want to call them out!
|
||||
@@ -421,6 +508,7 @@ 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.
|
||||
@@ -429,9 +517,13 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **[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.
|
||||
- **WH6GXZ nurse dude**: For bashing on installer
|
||||
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **dj505**: trying it on windows!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Iris**: testing and finding 🐞
|
||||
- **Cisien, bitflip, Woof, propstg, trs2982, Josh, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
|
||||
166
config.template
166
config.template
@@ -32,6 +32,14 @@ autoPingInChannel = False
|
||||
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
|
||||
# require explicit command, the message will only be processed if it starts with a command word
|
||||
explicitCmd = True
|
||||
# list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
|
||||
favoriteNodeList =
|
||||
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
@@ -52,10 +60,15 @@ wikipedia = True
|
||||
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = False
|
||||
# Ollama model to use (defaults to gemma2:2b)
|
||||
# ollamaModel = llama3.1
|
||||
# Ollama model to use (defaults to gemma3:270m)
|
||||
# ollamaModel = gemma3:latest
|
||||
# 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
|
||||
# if True, the input is sent raw to the LLM, if False uses legacy template query
|
||||
rawLLMQuery = True
|
||||
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
@@ -80,6 +93,14 @@ sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
#Do not retry enabling interface if it fails, just exit to let OS restart the bot
|
||||
dont_retry_disconnect = False
|
||||
|
||||
#echo command, will echo back your message as the bot
|
||||
enableEcho = False
|
||||
# command will only echo 1:1 if sent on this channel, otherwise it will prepend @yourname
|
||||
echoChannel = 9
|
||||
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = False
|
||||
@@ -93,12 +114,27 @@ 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
|
||||
# device interface and channel to send the alert message to
|
||||
SentryInterface = 1
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
sentryIgnoreList =
|
||||
# Enable detection sensor alert, requires external sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
# Altitude in meters to trigger the alert
|
||||
highFlyingAlertAltitude = 2000
|
||||
# check with OpenSkyNetwork if highfly detected for aircraft
|
||||
highflyOpenskynetwork = True
|
||||
# Channel to send Alert when the high flying node is detected
|
||||
highFlyingAlertInterface = 1
|
||||
highFlyingAlertChannel = 2
|
||||
# list of nodes numbers to ignore high flying alert ex: 2813308004,4258675309
|
||||
highFlyingIgnoreList =
|
||||
|
||||
[bbs]
|
||||
enabled = True
|
||||
@@ -109,7 +145,9 @@ 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 =
|
||||
bbslink_whitelist =
|
||||
# enable API script access (increases disk i/o)
|
||||
bbsAPI_enabled = False
|
||||
|
||||
# location module
|
||||
[location]
|
||||
@@ -120,37 +158,64 @@ lon = -123.0
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
# repeaterList lookup location (rbook / artsci / False)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# 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 Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
coastalEnabled = False
|
||||
# Find the correct costal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# pz = Puget Sound, ph = Honolulu HI, gm = Florida Keys, pk = Alaska
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
# myCoastalZone is the .txt file with the forecast data
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt
|
||||
# number of data points to return, default is 3
|
||||
coastalForecastDays = 3
|
||||
|
||||
# EAS Alert Broadcast
|
||||
# NOAA USGS Hydrology river identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverList =
|
||||
|
||||
# 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 IPAWS/CAP Alert Broadcast
|
||||
# Goverment Alert Broadcast defaults to FEMA IPAWS
|
||||
eAlertBroadcastEnabled = False
|
||||
# Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
# Goverment Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# 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
|
||||
enableGBalerts = False
|
||||
# Enable Ignore, headline that includes following word list
|
||||
ignoreFEMAenable = True
|
||||
ignoreFEMAwords = test,exercise
|
||||
|
||||
# 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/
|
||||
@@ -158,6 +223,20 @@ 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]
|
||||
enabled = False
|
||||
@@ -170,6 +249,17 @@ 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
|
||||
@@ -187,14 +277,22 @@ signalCycleLimit = 5
|
||||
|
||||
[fileMon]
|
||||
filemon_enabled = False
|
||||
# text file to monitor for changes
|
||||
file_path = alert.txt
|
||||
# channel to send the message to can be 2,3 multiple channels comma separated
|
||||
broadcastCh = 2
|
||||
|
||||
# news command will return the contents of a text file
|
||||
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 the use of exernal shell commands, this enables some data in `sysinfo`
|
||||
enable_runShellCmd = False
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs
|
||||
allowXcmd = False
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
@@ -225,6 +323,7 @@ IMAP_FOLDER = inbox
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
disable_emojis = False
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
@@ -232,17 +331,28 @@ blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
hangman = True
|
||||
hamtest = True
|
||||
tictactoe = True
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 1.2
|
||||
# delay in seconds for splits in messages to avoid message collision
|
||||
splitDelay = 0.0
|
||||
# delay in seconds for response to avoid message collision /throttling
|
||||
responseDelay = 2.2
|
||||
# delay in seconds for splits in messages to avoid message collision /throttling
|
||||
splitDelay = 2.5
|
||||
# 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. 233 is hard limit 2.5+ firmware
|
||||
maxBuffer = 220
|
||||
|
||||
|
||||
# Max limit buffer for radio testing
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
# Noisy Node Telemetry Logging and packet threshold
|
||||
noisyNodeLogging = False
|
||||
noisyTelemetryLimit = 5
|
||||
# Enable detailed packet logging all packets
|
||||
DEBUGpacket = False
|
||||
# metaPacket detailed logging, the filter negates the port ID
|
||||
debugMetadata = False
|
||||
metadataFilter = TELEMETRY_APP,POSITION_APP
|
||||
|
||||
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,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]
|
||||
|
||||
@@ -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):
|
||||
@@ -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>
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
@@ -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>
|
||||
"""
|
||||
|
||||
53
etc/send-environment-metrics.py
Normal file
53
etc/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()
|
||||
106
install.sh
106
install.sh
@@ -4,6 +4,7 @@
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "########################\n"
|
||||
@@ -80,6 +81,7 @@ sudo usermod -a -G bluetooth $USER
|
||||
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
|
||||
@@ -158,10 +160,10 @@ else
|
||||
fi
|
||||
|
||||
# if $1 is passed
|
||||
if [[ $1 == "mesh" ]]; then
|
||||
bot="mesh"
|
||||
elif [[ $1 == "pong" ]]; then
|
||||
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)"
|
||||
@@ -176,6 +178,7 @@ 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?
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
@@ -191,26 +194,30 @@ if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" |
|
||||
sudo usermod -a -G meshbot meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added meshbot 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"
|
||||
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
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
printf "\n service files updated\n"
|
||||
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
@@ -241,38 +248,57 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
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 "ollama pull gemma2:2b\n"
|
||||
printf "Total download is multi GB, recomend pi5/8GB or better for this\n"
|
||||
printf "ollama pull gemma3:270m\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 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)"
|
||||
# ask if want to install gemma3:latest
|
||||
printf "\n Ollama install done now we can install the gemma3:270m components\n"
|
||||
echo "Do you want to install the gemma3:270m components? (y/n)"
|
||||
read gemma
|
||||
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
|
||||
ollama pull gemma2:2b
|
||||
ollama pull gemma3:270m
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
# 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\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
|
||||
# ask if the user wants to edit the ollama service for API access
|
||||
if [[ -f /etc/systemd/system/ollama.service ]]; then
|
||||
printf "\nEdit /etc/systemd/system/ollama.service and add Environment=OLLAMA_HOST=0.0.0.0 for API? (y/n)"
|
||||
read editollama
|
||||
if [[ $(echo "${editollama}" | grep -i "^y") ]]; then
|
||||
replace="s|\[Service\]|\[Service\]\nEnvironment=\"OLLAMA_HOST=0.0.0.0\"|g"
|
||||
sudo sed -i "$replace" /etc/systemd/system/ollama.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart ollama.service
|
||||
printf "\nOllama service updated and restarted\n"
|
||||
fi
|
||||
# assume we want to enable ollama in config.ini
|
||||
if [[ -f config.ini ]]; then
|
||||
replace="s|ollama = False|ollama = True|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nOllama enabled in config.ini\n"
|
||||
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
|
||||
@@ -294,14 +320,26 @@ else
|
||||
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\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
|
||||
@@ -321,7 +359,7 @@ exit 0
|
||||
# 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_reporting.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
|
||||
@@ -336,5 +374,5 @@ exit 0
|
||||
|
||||
|
||||
# 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
|
||||
# add 'bee = True' to config.ini General section.
|
||||
# wget https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a -O bee.txt
|
||||
|
||||
@@ -26,8 +26,10 @@ elif [ "$1" == "html" ]; then
|
||||
python3 etc/report_generator.py
|
||||
elif [ "$1" == "html5" ]; then
|
||||
python3 etc/report_generator5.py
|
||||
elif [[ "$1" == add* ]]; then
|
||||
python3 script/addFav.py
|
||||
else
|
||||
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5)"
|
||||
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5) or addFav"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# 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
|
||||
@@ -19,13 +21,21 @@ 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)
|
||||
```
|
||||
# Set level for stdout handler
|
||||
stdout_handler.setLevel(logging.INFO)
|
||||
```
|
||||
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
|
||||
|
||||
There is a web-server module you can run `python modules/web.py` from the project root directory and it will serve up the web content.
|
||||
by default. http://localhost:8420
|
||||
```python
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
```
|
||||
|
||||
602
mesh_bot.py
602
mesh_bot.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
# Meshtastic Autoresponder MESH Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
# K7MHI Kelly Keeton 2025
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
@@ -15,15 +15,13 @@ from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# list of commands to remove from the default list for DM only
|
||||
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
|
||||
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe"]
|
||||
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
cmdHistory = [] # list to hold the command history for lheard and history commands
|
||||
msg_history = [] # list to hold the message history for the messages command
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
global cmdHistory
|
||||
global cmdHistory, msg_history
|
||||
#Auto response to messages
|
||||
message_lower = message.lower()
|
||||
bot_response = "🤖I'm sorry, I'm afraid I can't do that."
|
||||
@@ -42,20 +40,30 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
|
||||
"checkin": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checklist": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"chess": lambda: handle_gTnW(chess=True),
|
||||
"clearsms": lambda: handle_sms(message_from_id, message),
|
||||
"cmd": lambda: help_message,
|
||||
"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),
|
||||
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"echo": lambda: handle_echo(message, message_from_id, deviceID, isDM, channel_number),
|
||||
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"earthquake": lambda: handleEarthquake(message, message_from_id, deviceID),
|
||||
"email:": lambda: handle_email(message_from_id, message),
|
||||
"games": lambda: gamesCmdList,
|
||||
"globalthermonuclearwar": lambda: handle_gTnW(),
|
||||
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
|
||||
"hamtest": lambda: handleHamtest(message, message_from_id, deviceID),
|
||||
"hangman": lambda: handleHangman(message, message_from_id, deviceID),
|
||||
"hfcond": hf_band_conditions,
|
||||
"history": lambda: handle_history(message, message_from_id, deviceID, isDM),
|
||||
"howfar": lambda: handle_howfar(message, message_from_id, deviceID, isDM),
|
||||
"howtall": lambda: handle_howtall(message, message_from_id, deviceID, isDM),
|
||||
"joke": lambda: tell_joke(message_from_id),
|
||||
"lemonstand": lambda: handleLemonade(message, message_from_id, deviceID),
|
||||
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
@@ -63,6 +71,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
|
||||
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
|
||||
"motd": lambda: handle_motd(message, message_from_id, isDM),
|
||||
"mwx": lambda: handle_mwx(message_from_id, deviceID, channel_number),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
@@ -79,16 +88,19 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"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),
|
||||
"tictactoe": lambda: handleTicTacToe(message, message_from_id, deviceID),
|
||||
"tic-tac-toe": lambda: handleTicTacToe(message, message_from_id, deviceID),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"valert": lambda: get_volcano_usgs(),
|
||||
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"whois": lambda: handle_whois(message, deviceID, channel_number, message_from_id),
|
||||
"wiki:": lambda: handle_wiki(message, isDM),
|
||||
"wiki?": lambda: handle_wiki(message, isDM),
|
||||
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
|
||||
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"x:": lambda: handleShellCmd(message, message_from_id, channel_number, isDM, deviceID),
|
||||
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
|
||||
"📍": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"🔔": lambda: handle_alertBell(message_from_id, deviceID, message),
|
||||
@@ -110,6 +122,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
# check the message for commands words list, processed after system.messageTrap
|
||||
for key in command_handler:
|
||||
word = message_lower.split(' ')
|
||||
if cmdBang:
|
||||
# strip the !
|
||||
if word[0].startswith("!"):
|
||||
word[0] = word[0][1:]
|
||||
if key in word:
|
||||
# append all the commands found in the message to the cmds list
|
||||
cmds.append({'cmd': key, 'index': message_lower.index(key)})
|
||||
@@ -137,6 +153,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
time.sleep(responseDelay)
|
||||
return bot_response
|
||||
|
||||
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
|
||||
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
|
||||
@@ -278,6 +301,21 @@ def handle_motd(message, message_from_id, isDM):
|
||||
msg = "MOTD: " + MOTD
|
||||
return msg
|
||||
|
||||
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
|
||||
if "?" in message.lower():
|
||||
return "echo command returns your message back to you. Example:echo Hello World"
|
||||
elif "echo " in message.lower():
|
||||
parts = message.lower().split("echo ", 1)
|
||||
if len(parts) > 1 and parts[1].strip() != "":
|
||||
echo_msg = parts[1]
|
||||
if channel_number != echoChannel:
|
||||
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
|
||||
return echo_msg
|
||||
else:
|
||||
return "Please provide a message to echo back to you. Example:echo Hello World"
|
||||
else:
|
||||
return "Please provide a message to echo back to you. Example:echo Hello World"
|
||||
|
||||
def handle_wxalert(message_from_id, deviceID, message):
|
||||
if use_meteo_wxApi:
|
||||
return "wxalert is not supported"
|
||||
@@ -293,15 +331,77 @@ def handle_wxalert(message_from_id, deviceID, message):
|
||||
weatherAlert = weatherAlert[0]
|
||||
return weatherAlert
|
||||
|
||||
def handle_howfar(message, message_from_id, deviceID, isDM):
|
||||
msg = ''
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
lat = location[0]
|
||||
lon = location[1]
|
||||
# if ? in message
|
||||
if "?" in message.lower():
|
||||
return "command returns the distance you have traveled since your last HowFar-command. Add 'reset' to reset your starting point."
|
||||
|
||||
# if no GPS location return
|
||||
if lat == latitudeValue and lon == longitudeValue:
|
||||
logger.debug(f"System: HowFar: No GPS location for {message_from_id}")
|
||||
return "No GPS location available"
|
||||
|
||||
if "reset" in message.lower():
|
||||
msg = distance(lat,lon,message_from_id, reset=True)
|
||||
else:
|
||||
msg = distance(lat,lon,message_from_id)
|
||||
|
||||
# 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_howtall(message, message_from_id, deviceID, isDM):
|
||||
msg = ''
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
lat = location[0]
|
||||
lon = location[1]
|
||||
if lat == latitudeValue and lon == longitudeValue:
|
||||
logger.debug(f"System: HowTall: No GPS location for {message_from_id}")
|
||||
return "No GPS location available"
|
||||
if use_metric:
|
||||
measure = "meters"
|
||||
else:
|
||||
measure = "feet"
|
||||
# if ? in message
|
||||
if "?" in message.lower():
|
||||
return f"command estimates your height based on the shadow length you provide in {measure}. Example: howtall 5.5"
|
||||
# get the shadow length from the message split after howtall
|
||||
try:
|
||||
shadow_length = float(message.lower().split("howtall ")[1].split(" ")[0])
|
||||
except:
|
||||
return f"Please provide a shadow length in {measure} example: howtall 5.5"
|
||||
|
||||
# get data
|
||||
msg = measureHeight(lat, lon, shadow_length)
|
||||
|
||||
# if data has NO_ALERTS return help
|
||||
if NO_ALERTS in msg:
|
||||
return f"Please provide a shadow length in {measure} example: howtall 5.5"
|
||||
|
||||
return msg
|
||||
|
||||
def handle_wiki(message, isDM):
|
||||
# location = get_node_location(message_from_id, deviceID)
|
||||
msg = "Wikipedia search function. \nUsage example:📲wiki: travelling gnome"
|
||||
if "wiki:" in message.lower():
|
||||
search = message.split(":")[1]
|
||||
search = search.strip()
|
||||
if search:
|
||||
return get_wikipedia_summary(search)
|
||||
return "Please add a search term example:📲wiki: travelling gnome"
|
||||
try:
|
||||
if "wiki:?" in message.lower() or "wiki: ?" in message.lower() or "wiki?" in message.lower() or "wiki ?" in message.lower():
|
||||
return msg
|
||||
if "wiki" in message.lower():
|
||||
search = message.split(":")[1]
|
||||
search = search.strip()
|
||||
if search:
|
||||
return get_wikipedia_summary(search)
|
||||
return "Please add a search term example:📲wiki: travelling gnome"
|
||||
except Exception as e:
|
||||
logger.error(f"System: Wiki Exception {e}")
|
||||
msg = "Error processing your request"
|
||||
|
||||
return msg
|
||||
|
||||
# Runtime Variables for LLM
|
||||
@@ -338,8 +438,9 @@ def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
return passes
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory, seenNodes
|
||||
location_name = 'no location provided'
|
||||
msg = ''
|
||||
|
||||
if location_enabled:
|
||||
# if message_from_id is is the llmLocationTable use the location from the list to save on API calls
|
||||
@@ -365,17 +466,20 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# consider this a command use for the cmdHistory list
|
||||
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
|
||||
|
||||
# if the message_from_id is not in the llmLocationTable send the welcome message
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# check for a welcome message (is this redundant?)
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
node['welcome'] = True
|
||||
|
||||
# update the llmLocationTable for future use
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
@@ -394,26 +498,18 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# information for the user on how long the query will take on average
|
||||
if llmRunCounter > 0:
|
||||
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
|
||||
if averageRuntime > 25:
|
||||
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
msg = f"Average query time is: {int(averageRuntime)} seconds" if averageRuntime > 25 else ''
|
||||
else:
|
||||
msg = "Please wait, response could take 30+ seconds. Fund the SysOp's GPU budget!"
|
||||
|
||||
if msg != '':
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
start = time.time()
|
||||
|
||||
@@ -449,12 +545,17 @@ def handleDopeWars(message, nodeID, rxNode):
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handle_gTnW():
|
||||
def handle_gTnW(chess = False):
|
||||
chess = ["How about a nice game of chess?", "Shall we play a game of chess?", "Would you like to play a game of chess?", "f3, to e5, g4??"]
|
||||
response = ["The only winning move is not to play.", "What are you doing, Dave?",\
|
||||
"Greetings, Professor Falken.", "Shall we play a game?", "How about a nice game of chess?",\
|
||||
"You are a hard man to reach. Could not find you in Seattle and no terminal is in operation at your classified address.",\
|
||||
"I should reach Defcon 1 and release my missiles in 28 hours.","T-minus thirty","Malfunction 54: Treatment pause;dose input 2", "reticulating splines"]
|
||||
length = len(response)
|
||||
chess_length = len(chess)
|
||||
if chess:
|
||||
response = chess
|
||||
length = chess_length
|
||||
indices = list(range(length))
|
||||
# Shuffle the indices using a convoluted method
|
||||
for i in range(length):
|
||||
@@ -652,29 +753,122 @@ def handleGolf(message, nodeID, deviceID):
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handleHangman(message, nodeID, deviceID):
|
||||
global hangmanTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
for i in range(len(hangmanTracker)):
|
||||
if hangmanTracker[i]['nodeID'] == nodeID:
|
||||
hangmanTracker[i]["last_played"] = time.time()
|
||||
index = i+1
|
||||
break
|
||||
|
||||
if index and "end" in message.lower():
|
||||
hangman.end(nodeID)
|
||||
hangmanTracker.pop(index-1)
|
||||
return "Thanks for hanging out🤙"
|
||||
|
||||
if not index:
|
||||
hangmanTracker.append(
|
||||
{
|
||||
"nodeID": nodeID,
|
||||
"last_played": time.time()
|
||||
}
|
||||
)
|
||||
msg = "🧩Hangman🤖 'end' to cut rope🪢\n"
|
||||
msg += hangman.play(nodeID, message)
|
||||
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handleHamtest(message, nodeID, deviceID):
|
||||
global hamtestTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
response = message.split(' ')
|
||||
for i in range(len(hamtestTracker)):
|
||||
if hamtestTracker[i]['nodeID'] == nodeID:
|
||||
hamtestTracker[i]["last_played"] = time.time()
|
||||
index = i+1
|
||||
break
|
||||
|
||||
if not index:
|
||||
hamtestTracker.append({"nodeID": nodeID,"last_played": time.time()})
|
||||
|
||||
if "end" in response[0].lower():
|
||||
msg = hamtest.endGame(nodeID)
|
||||
elif "score" in response[0].lower():
|
||||
msg = hamtest.getScore(nodeID)
|
||||
|
||||
if "hamtest" in response[0].lower():
|
||||
if len(response) > 1:
|
||||
if "gen" in response[1].lower():
|
||||
msg = hamtest.newGame(nodeID, 'general')
|
||||
elif "ex" in response[1].lower():
|
||||
msg = hamtest.newGame(nodeID, 'extra')
|
||||
else:
|
||||
msg = hamtest.newGame(nodeID, 'technician')
|
||||
|
||||
# if the message is an answer A B C or D upper or lower case
|
||||
if response[0].upper() in ['A', 'B', 'C', 'D']:
|
||||
msg = hamtest.answer(nodeID, response[0])
|
||||
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handleTicTacToe(message, nodeID, deviceID):
|
||||
global tictactoeTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
|
||||
# Find or create player tracker entry
|
||||
for i in range(len(tictactoeTracker)):
|
||||
if tictactoeTracker[i]['nodeID'] == nodeID:
|
||||
tictactoeTracker[i]["last_played"] = time.time()
|
||||
index = i+1
|
||||
break
|
||||
|
||||
if message.lower().startswith('e'):
|
||||
if index:
|
||||
tictactoe.end(nodeID)
|
||||
tictactoeTracker.pop(index-1)
|
||||
return "Thanks for playing! 🎯"
|
||||
|
||||
if not index:
|
||||
tictactoeTracker.append({
|
||||
"nodeID": nodeID,
|
||||
"last_played": time.time()
|
||||
})
|
||||
msg = "🎯Tic-Tac-Toe🤖 '(e)nd'\n"
|
||||
|
||||
msg += tictactoe.play(nodeID, message)
|
||||
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handle_riverFlow(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
userRiver = message.lower()
|
||||
|
||||
if "riverflow " in userRiver:
|
||||
userRiver = userRiver.split("riverflow ")[1] if "riverflow " in userRiver else riverListDefault
|
||||
if "riverflow " in message.lower() and "," in message:
|
||||
userRiver = message.lower().split("riverflow ", 1)[1].strip()
|
||||
userRiver = [r.strip() for r in userRiver.split(",") if r.strip()]
|
||||
else:
|
||||
userRiver = userRiver.split(",") if "," in userRiver else riverListDefault
|
||||
userRiver = riverListDefault
|
||||
|
||||
# return river flow data
|
||||
if use_meteo_wxApi:
|
||||
return get_flood_openmeteo(location[0], location[1])
|
||||
else:
|
||||
# if userRiver a list
|
||||
if type(userRiver) == list:
|
||||
msg = ""
|
||||
for river in userRiver:
|
||||
msg += get_flood_noaa(location[0], location[1], river)
|
||||
return msg
|
||||
# if single river
|
||||
msg = get_flood_noaa(location[0], location[1], userRiver)
|
||||
msg = ""
|
||||
for river in userRiver:
|
||||
msg += get_flood_noaa(location[0], location[1], river)
|
||||
return msg
|
||||
|
||||
def handle_mwx(message_from_id, deviceID, cmd):
|
||||
# NOAA Coastal and Marine Weather
|
||||
if myCoastalZone is None:
|
||||
logger.warning("System: Coastal Zone not set, please set in config.ini")
|
||||
return NO_ALERTS
|
||||
return get_nws_marine(zone=myCoastalZone, days=coastalForecastDays)
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
@@ -694,9 +888,9 @@ def handle_wxc(message_from_id, deviceID, cmd):
|
||||
|
||||
def handle_emergency_alerts(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if enableGBalerts:
|
||||
# UK Alerts
|
||||
return get_govUK_alerts(str(location[0]), str(location[1]))
|
||||
if enableDEalerts:
|
||||
# nina Alerts
|
||||
return get_nina_alerts()
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
@@ -704,6 +898,16 @@ def handle_emergency_alerts(message, message_from_id, deviceID):
|
||||
# Headlines only FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
|
||||
|
||||
def handleEarthquake(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if "earthquake" in message.lower():
|
||||
return checkUSGSEarthQuake(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_checklist(message, message_from_id, deviceID):
|
||||
name = get_name_from_number(message_from_id, 'short', deviceID)
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
return process_checklist_command(message_from_id, message, name, location)
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
if "$" in message and not "example:" in message:
|
||||
subject = message.split("$")[1].split("#")[0]
|
||||
@@ -724,7 +928,7 @@ def handle_bbspost(message, message_from_id, deviceID):
|
||||
toNode = int(toNode.strip("!"),16)
|
||||
except ValueError as e:
|
||||
toNode = 0
|
||||
elif toNode.isalpha() or not toNode.isnumeric():
|
||||
elif toNode.isalpha() or not toNode.isnumeric() or len(toNode) < 5:
|
||||
# try short name
|
||||
toNode = get_num_from_short_name(toNode, deviceID)
|
||||
|
||||
@@ -757,14 +961,19 @@ def handle_messages(message, deviceID, channel_number, msg_history, publicChanne
|
||||
return message.split("?")[0].title() + " command returns the last " + str(storeFlimit) + " messages sent on a channel."
|
||||
else:
|
||||
response = ""
|
||||
for msgH in msg_history:
|
||||
# Reverse the message history to show most recent first
|
||||
for msgH in reversed(msg_history):
|
||||
# number of messages to return +1 for the header line
|
||||
if len(response.split("\n")) >= storeFlimit + 1:
|
||||
break
|
||||
# if the message is for this deviceID and channel or publicChannel
|
||||
if msgH[4] == deviceID:
|
||||
if msgH[2] == channel_number or msgH[2] == publicChannel:
|
||||
response += f"\n{msgH[0]}: {msgH[1]}"
|
||||
if len(response) > 0:
|
||||
return "Message History:" + response
|
||||
return "📨Messages:" + response
|
||||
else:
|
||||
return "No messages in history"
|
||||
return "No 📭messages in history"
|
||||
|
||||
def handle_sun(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
@@ -775,8 +984,14 @@ def sysinfo(message, message_from_id, deviceID):
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
if enable_runShellCmd and file_monitor_enabled:
|
||||
shellData = call_external_script(None, "script/sysEnv.sh").rstrip()
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData
|
||||
# get the system information from the shell script
|
||||
# this is an example of how to run a shell script and return the data
|
||||
shellData = call_external_script(None, "script/sysEnv.sh")
|
||||
# check if the script returned data
|
||||
if shellData == "" or shellData == None:
|
||||
# no data returned from the script
|
||||
shellData = "shell script data missing"
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData.rstrip()
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
@@ -817,7 +1032,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
|
||||
prettyTime = getPrettyTime(cmdTime)
|
||||
|
||||
# history display output
|
||||
if nodeid in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
if str(nodeid) in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(cmdHistory[i]['nodeID'], 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
elif cmdHistory[i]['nodeID'] == nodeid and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(nodeid, 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
@@ -876,7 +1091,6 @@ def handle_moon(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_moon(str(location[0]), str(location[1]))
|
||||
|
||||
|
||||
def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
|
||||
try:
|
||||
loc = []
|
||||
@@ -965,13 +1179,17 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
game = "None"
|
||||
|
||||
trackers = [
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars),
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade),
|
||||
(vpTracker, "VideoPoker", handleVideoPoker),
|
||||
(jackTracker, "BlackJack", handleBlackJack),
|
||||
(mindTracker, "MasterMind", handleMmind),
|
||||
(golfTracker, "GolfSim", handleGolf),
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
|
||||
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
|
||||
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
|
||||
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
|
||||
]
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
|
||||
@@ -981,7 +1199,7 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
return playingGame
|
||||
|
||||
def onReceive(packet, interface):
|
||||
global seenNodes
|
||||
global seenNodes, msg_history, cmdHistory
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
@@ -994,6 +1212,7 @@ def onReceive(packet, interface):
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
isDM = False
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
@@ -1014,19 +1233,18 @@ def onReceive(packet, interface):
|
||||
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 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 rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
@@ -1056,20 +1274,22 @@ def onReceive(packet, interface):
|
||||
if msg:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
logger.info(f"System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
|
||||
bbs_delete_dm(msg[0], msg[1])
|
||||
send_message(message, channel_number, message_from_id, rxNode)
|
||||
|
||||
|
||||
# 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')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
rx_time = packet['decoded'].get('rxTime', time.time())
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay detected")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
@@ -1103,15 +1323,17 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if DEBUGhops:
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
logger.debug(f"System: Packet HopDebugger: No hop count found in PACKET {packet} END PACKET")
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
@@ -1135,7 +1357,7 @@ def onReceive(packet, interface):
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
# log the message to stdout
|
||||
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
|
||||
@@ -1153,7 +1375,7 @@ def onReceive(packet, interface):
|
||||
playingGame = False
|
||||
|
||||
if not playingGame:
|
||||
if llm_enabled:
|
||||
if llm_enabled and llmReplyToNonCommands:
|
||||
# respond with LLM
|
||||
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
@@ -1182,14 +1404,22 @@ def onReceive(packet, interface):
|
||||
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-'))
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for us to respond to, or is it...
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
|
||||
elif str(message_from_id) in bbs_ban_list:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
|
||||
elif str(channel_number) in ignoreChannels:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
|
||||
elif cmdBang and not message_string.startswith("!"):
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
|
||||
else:
|
||||
# message is for bot to respond to
|
||||
# message is for bot to respond to, seriously this time..
|
||||
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:
|
||||
@@ -1208,18 +1438,20 @@ def onReceive(packet, interface):
|
||||
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
|
||||
# message is not for us to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
|
||||
|
||||
if len(msg_history) < storeFlimit:
|
||||
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
|
||||
else:
|
||||
msg_history.pop(0)
|
||||
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
|
||||
|
||||
# trim the history list if it exceeds max_history
|
||||
if len(msg_history) >= MAX_MSG_HISTORY:
|
||||
# Remove oldest entries by cutting in half
|
||||
msg_history = msg_history[len(msg_history)//2:]
|
||||
|
||||
# add the message to the history list
|
||||
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
|
||||
|
||||
# print the message to the log and sdout
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
|
||||
@@ -1239,9 +1471,25 @@ def onReceive(packet, interface):
|
||||
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, i)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# if QRZ enabled check if we have said hello
|
||||
if qrz_hello_enabled:
|
||||
if never_seen_before(message_from_id):
|
||||
name = get_name_from_number(message_from_id, 'short', rxNode)
|
||||
if isinstance(name, str) and name.startswith("!") and len(name) == 9:
|
||||
# we didnt get a info packet yet so wait and ingore this go around
|
||||
logger.debug(f"System: QRZ Hello ignored, no info packet yet")
|
||||
else:
|
||||
# add to qrz_hello list
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode, channel_number)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
@@ -1261,8 +1509,9 @@ async def start_rx():
|
||||
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
|
||||
llm_query(" ")
|
||||
logger.debug(f"System: LLM model {llmModel} loaded")
|
||||
llmLoad = llm_query(" ")
|
||||
if "trouble" not in llmLoad:
|
||||
logger.debug(f"System: LLM Model {llmModel} loaded")
|
||||
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
@@ -1284,6 +1533,8 @@ async def start_rx():
|
||||
logger.debug("System: Location Telemetry Enabled using NOAA API")
|
||||
if dad_jokes_enabled:
|
||||
logger.debug("System: Dad Jokes Enabled!")
|
||||
if coastalEnabled:
|
||||
logger.debug("System: Coastal Forcast and Tide Enabled!")
|
||||
if games_enabled:
|
||||
logger.debug("System: Games Enabled!")
|
||||
if wikipedia_enabled:
|
||||
@@ -1292,18 +1543,24 @@ async def start_rx():
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if enableEcho:
|
||||
logger.debug(f"System: Echo command Enabled")
|
||||
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}")
|
||||
logger.warning(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
|
||||
if enable_runShellCmd:
|
||||
logger.debug(f"System: Shell Command monitor enabled")
|
||||
if allowXcmd and enable_runShellCmd:
|
||||
logger.warning(f"System: File Monitor shell XCMD Enabled")
|
||||
if read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if bee_enabled:
|
||||
@@ -1311,26 +1568,87 @@ async def start_rx():
|
||||
if wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh} for FIPS codes {myStateFIPSList}")
|
||||
# check if the FIPS codes are set
|
||||
if myStateFIPSList == ['']:
|
||||
logger.warning(f"System: No FIPS codes set for iPAWS Alerts")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Volcano Alert Broadcast Enabled on channels {volcanoAlertBroadcastChannel}")
|
||||
if qrz_hello_enabled and train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled with training mode")
|
||||
if qrz_hello_enabled and not train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled")
|
||||
if checklist_enabled:
|
||||
logger.debug(f"System: CheckList Module Enabled")
|
||||
if ignoreChannels != []:
|
||||
logger.debug(f"System: Ignoring Channels: {ignoreChannels}")
|
||||
if noisyNodeLogging:
|
||||
logger.debug(f"System: Noisy Node Logging Enabled")
|
||||
if enableSMTP:
|
||||
if enableImap:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled using IMAP")
|
||||
else:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled")
|
||||
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"))
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
# basic scheduler
|
||||
if schedulerValue != '':
|
||||
logger.debug(f"System: Starting the broadcast scheduler from config.ini")
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
# Send a message every day at the time set in schedulerTime
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
# Send a message every day at the time set in schedulerInterval
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Monday at the time set in schedulerTime
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Tuesday at the time set in schedulerTime
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Wednesday at the time set in schedulerTime
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Thursday at the time set in schedulerTime
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Friday at the time set in schedulerTime
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Saturday at the time set in schedulerTime
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Sunday at the time set in schedulerTime
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
# Send a message every hour at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
# Send a message every minute at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
logger.debug(f"System: Starting the broadcast scheduler")
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
@@ -1352,7 +1670,6 @@ async def start_rx():
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
@@ -1362,25 +1679,52 @@ async def start_rx():
|
||||
|
||||
# Hello World
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
if file_monitor_enabled:
|
||||
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
|
||||
if radio_detection_enabled:
|
||||
hamlibTask = asyncio.create_task(handleSignalWatcher())
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
if radio_detection_enabled:
|
||||
await asyncio.gather(hamlibTask)
|
||||
if file_monitor_enabled:
|
||||
await asyncio.gather(fileMonTask)
|
||||
tasks = []
|
||||
|
||||
try:
|
||||
# Create core tasks
|
||||
tasks.append(asyncio.create_task(start_rx(), name="mesh_rx"))
|
||||
tasks.append(asyncio.create_task(watchdog(), name="watchdog"))
|
||||
|
||||
# Add optional tasks
|
||||
if file_monitor_enabled:
|
||||
tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor"))
|
||||
|
||||
if radio_detection_enabled:
|
||||
tasks.append(asyncio.create_task(handleSignalWatcher(), name="hamlib"))
|
||||
|
||||
logger.debug(f"System: Starting {len(tasks)} async tasks")
|
||||
|
||||
# Wait for all tasks with proper exception handling
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Check for exceptions in results
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Task {tasks[i].get_name()} failed with: {result}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Main loop error: {e}")
|
||||
finally:
|
||||
# Cleanup tasks
|
||||
logger.debug("System: Cleaning up async tasks")
|
||||
for task in tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"Task {task.get_name()} cancelled successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cancelling task {task.get_name()}: {e}")
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
except SystemExit:
|
||||
pass
|
||||
# EOF
|
||||
|
||||
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.
|
||||
@@ -16,19 +16,37 @@ def load_bbsdb():
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except Exception as e:
|
||||
new_bbs_messages = pickle.load(f)
|
||||
if isinstance(new_bbs_messages, list):
|
||||
for msg in new_bbs_messages:
|
||||
#example [1, 'Welcome to meshBBS', 'Welcome to the BBS, please post a message!', 0]
|
||||
msgHash = hash(tuple(msg[1:3])) # Create a hash of the message content (subject and body)
|
||||
# Check if the message already exists in bbs_messages
|
||||
if all(hash(tuple(existing_msg[1:3])) != msgHash for existing_msg in bbs_messages):
|
||||
# if the message is not a duplicate, add it to bbs_messages Maintain the message ID sequence
|
||||
new_id = len(bbs_messages) + 1
|
||||
bbs_messages.append([new_id, msg[1], msg[2], msg[3]])
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: bbsdb.pkl not found, creating new one")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error creating bbsdb.pkl: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error loading bbsdb.pkl: {e}")
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
logger.debug("System: Creating new data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
def save_bbsdb():
|
||||
global bbs_messages
|
||||
# save the bbs messages to the database file
|
||||
logger.debug("System: Saving data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
try:
|
||||
logger.debug("System: Saving data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error saving bbsdb: {e}")
|
||||
|
||||
def bbs_help():
|
||||
# help message
|
||||
@@ -40,7 +58,7 @@ def bbs_list_messages():
|
||||
message_list = ""
|
||||
for message in bbs_messages:
|
||||
# message[0] is the messageID, message[1] is the subject
|
||||
message_list += "Msg #" + str(message[0]) + " " + message[1] + "\n"
|
||||
message_list += "[#" + str(message[0]) + "] " + message[1] + "\n"
|
||||
|
||||
# last newline removed
|
||||
message_list = message_list[:-1]
|
||||
@@ -99,8 +117,10 @@ def bbs_read_message(messageID = 0):
|
||||
if (messageID - 1) >= len(bbs_messages):
|
||||
return "Message not found."
|
||||
if messageID > 0:
|
||||
fromNode = bbs_messages[messageID - 1][3]
|
||||
fromNodeHex = hex(fromNode)[-4:]
|
||||
message = bbs_messages[messageID - 1]
|
||||
return f"Msg #{message[0]}\nMsg Body: {message[2]}"
|
||||
return f"Msg #{message[0]}\nFrom:{fromNodeHex}\n{message[2]}"
|
||||
else:
|
||||
return "Please specify a message number to read."
|
||||
|
||||
@@ -116,7 +136,11 @@ def load_bbsdm():
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('data/bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
new_bbs_dm = pickle.load(f)
|
||||
if isinstance(new_bbs_dm, list):
|
||||
for msg in new_bbs_dm:
|
||||
if msg not in bbs_dm:
|
||||
bbs_dm.append(msg)
|
||||
except:
|
||||
bbs_dm = [[1234567890, "Message", 1234567890]]
|
||||
logger.debug("System: Creating new data/bbsdm.pkl")
|
||||
@@ -167,7 +191,7 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
# check if the bbs link is enabled
|
||||
if bbs_link_whitelist is not None:
|
||||
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."
|
||||
@@ -180,21 +204,34 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
#store the message
|
||||
subject = input.split("$")[1].split("#")[0]
|
||||
body = input.split("#")[1]
|
||||
bbs_post_message(subject, body, peerNode)
|
||||
fromNodeHex = input.split("@")[1]
|
||||
try:
|
||||
bbs_post_message(subject, body, int(fromNodeHex, 16))
|
||||
except:
|
||||
logger.error(f"System: Error parsing bbslink from node {peerNode}: {input}")
|
||||
fromNodeHex = hex(peerNode)
|
||||
messageID = input.split(" ")[1]
|
||||
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 with delay to keep chutil happy
|
||||
if messageID < len(bbs_messages):
|
||||
logger.debug(f"System: wait to bbslink with peer " + str(peerNode))
|
||||
fromNodeHex = hex(bbs_messages[messageID][3])
|
||||
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]}"
|
||||
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
|
||||
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
|
||||
else:
|
||||
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
|
||||
|
||||
|
||||
179
modules/checklist.py
Normal file
179
modules/checklist.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# 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, including days
|
||||
total_seconds = time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))
|
||||
days = int(total_seconds // 86400)
|
||||
hours = int((total_seconds % 86400) // 3600)
|
||||
minutes = int((total_seconds % 3600) // 60)
|
||||
seconds = int(total_seconds % 60)
|
||||
if days > 0:
|
||||
timeCheckedIn = f"{days}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||
else:
|
||||
timeCheckedIn = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
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 "?" in message.lower():
|
||||
if not reverse_in_out:
|
||||
return ("Command: checklist followed by\n"
|
||||
"checkout to check out\n"
|
||||
"purgeout to delete your checkout record\n"
|
||||
"Example: checkin Arrived at park")
|
||||
else:
|
||||
return ("Command: checklist followed by\n"
|
||||
"checkin to check out\n"
|
||||
"purgeout to delete your checkin record\n"
|
||||
"Example: checkout Leaving park")
|
||||
elif "checklist" in message.lower():
|
||||
return list_checkin()
|
||||
else:
|
||||
return "Invalid command."
|
||||
@@ -5,6 +5,7 @@ from modules.log import *
|
||||
import asyncio
|
||||
import random
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
@@ -17,12 +18,12 @@ def read_file(file_monitor_file_path, random_line_only=False):
|
||||
return "🐝buzz 💐buzz buzz🍯"
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
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') as f:
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
@@ -31,13 +32,13 @@ def read_file(file_monitor_file_path, random_line_only=False):
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path, read_news_enabled)
|
||||
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') as f:
|
||||
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
|
||||
@@ -46,7 +47,7 @@ def write_news(content, append=False):
|
||||
return False
|
||||
|
||||
async def watch_file():
|
||||
|
||||
# Watch the file for changes and return the new content when it changes
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
return None
|
||||
else:
|
||||
@@ -64,6 +65,7 @@ async def watch_file():
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
# Call an external script with the message as an argument this is a example only
|
||||
try:
|
||||
# Debugging: Print the current working directory and resolved script path
|
||||
current_working_directory = os.getcwd()
|
||||
@@ -76,9 +78,44 @@ def call_external_script(message, script="script/runShell.sh"):
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
def handleShellCmd(message, message_from_id, channel_number, isDM, deviceID):
|
||||
if not allowXcmd:
|
||||
return "x: command is disabled"
|
||||
|
||||
if str(message_from_id) not in bbs_admin_list:
|
||||
logger.warning(f"FileMon: Unauthorized x: command attempt from {message_from_id}")
|
||||
return "x: command not authorized"
|
||||
|
||||
if not isDM:
|
||||
return "x: command not authorized in group chat"
|
||||
|
||||
if enable_runShellCmd:
|
||||
# clean up the command input
|
||||
if message.lower().startswith("x:"):
|
||||
command = message[2:]
|
||||
if command.startswith(" "):
|
||||
command = command[1:]
|
||||
command = command.strip()
|
||||
else:
|
||||
return "x: invalid command format"
|
||||
# Run the shell command as a subprocess
|
||||
try:
|
||||
logger.info(f"FileMon: Running shell command from {message_from_id}: {command}")
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10, start_new_session=True)
|
||||
output = result.stdout.strip()
|
||||
if output:
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error running shell command: {e}")
|
||||
logger.debug(f"FileMon: This command is not good for use over the mesh network")
|
||||
else:
|
||||
logger.debug("FileMon: x: command is disabled by no enable_runShellCmd")
|
||||
return "x: command is disabled"
|
||||
|
||||
return "x: command executed with no output"
|
||||
|
||||
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()
|
||||
@@ -2,8 +2,59 @@
|
||||
# The emoji table of contents is used to replace words in the joke with emojis
|
||||
# As a Ham, is this obsecuring the meaning of the joke? Or is it enhancing it?
|
||||
from dadjokes import Dadjoke # pip install dadjokes
|
||||
import random
|
||||
from modules.log import *
|
||||
|
||||
lameJokes = [
|
||||
"Why don't scientists trust atoms? Because they make up everything!",
|
||||
"Why did the scarecrow win an award? Because he was outstanding in his field!",
|
||||
"Why don't skeletons fight each other? They don't have the guts.",
|
||||
"What do you call fake spaghetti? An impasta!",
|
||||
"Why did the bicycle fall over? Because it was two-tired!",
|
||||
"Why did the math book look sad? Because it had too many problems.",
|
||||
"Why did the golfer bring two pairs of pants? In case he got a hole in one.",
|
||||
"Why did the coffee file a police report? It got mugged.",
|
||||
"Why did the tomato turn red? Because it saw the salad dressing!",
|
||||
"Why did the cookie go to the doctor? Because it felt crummy.",
|
||||
"Why did the computer go to the doctor? Because it had a virus!",
|
||||
"Why did the chicken join a band? Because it had the drumsticks!",
|
||||
"Why did the banana go to the doctor? Because it wasn't peeling well.",
|
||||
"Why did the cow go to space? To see the moooon!",
|
||||
"Why did the fish blush? Because it saw the ocean's bottom!",
|
||||
"Why did the elephant bring a suitcase to the zoo? Because it wanted to pack its trunk!",
|
||||
"Why did the meshtastic node go to therapy? It had too many connections to handle!",
|
||||
"Why did the meshtastic user bring a ladder to the meeting? To reach new heights in communication!",
|
||||
"Why did the meshtastic device break up with Wi-Fi? It found a better connection!",
|
||||
"Why did the meshtastic network throw a party? Because it wanted to mesh well with everyone!",
|
||||
"Why did the meshtastic node get promoted? Because it was outstanding in its field!",
|
||||
"Why did the meshtastic user bring a map? To navigate the mesh of possibilities!",
|
||||
"Why did the meshtastic device go to school? To improve its signal strength!",
|
||||
"How did the meshtastic node become a comedian? It uses mesh-bots to deliver punchlines!",
|
||||
"Chuck Norris doesn't read books. He stares them down until he gets the information he wants.",
|
||||
"When Chuck Norris enters a room, he doesn't turn the lights on. He turns the dark off.",
|
||||
"Chuck Norris can divide by zero.",
|
||||
"Chuck Norris counted to infinity. Twice.",
|
||||
"Chuck Norris can slam a revolving door.",
|
||||
"When Chuck Norris does a push-up, he isn't lifting himself up; he's pushing the Earth down.",
|
||||
"Chuck Norris can hear sign language.",
|
||||
"Death once had a near-Chuck Norris experience.",
|
||||
"Chuck Norris can unscramble an egg.",
|
||||
"Chuck Norris can win a game of Connect Four in only three moves.",
|
||||
"Chuck Norris can make a snowman out of rain.",
|
||||
"Chuck Norris can strangle you with a cordless phone.",
|
||||
"Chuck Norris can do a wheelie on a unicycle.",
|
||||
"Chuck Norris can kill two stones with one bird.",
|
||||
"Chuck Norris can speak braille.",
|
||||
"Chuck Norris can build a snowman out of rain.",
|
||||
"Chuck Norris can hear sign language.",
|
||||
"Death once had a near-Chuck Norris experience.",
|
||||
"Chuck Norris can unscramble an egg.",
|
||||
"Chuck Norris can win a game of Connect Four in only three moves.",
|
||||
"Chuck Norris can make a snowman out of rain.",
|
||||
"Chuck Norris can strangle you with a cordless phone.",
|
||||
"Chuck Norris can do a wheelie on a unicycle.",
|
||||
"Chuck Norris can kill two stones with one bird."]
|
||||
|
||||
def tableOfContents():
|
||||
wordToEmojiMap = {
|
||||
'love': '❤️', 'heart': '❤️', 'happy': '😊', 'smile': '😊', 'sad': '😢', 'angry': '😠', 'mad': '😠', 'cry': '😢', 'laugh': '😂', 'funny': '😂', 'cool': '😎',
|
||||
@@ -117,10 +168,12 @@ def sendWithEmoji(message):
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
dadjoke = Dadjoke()
|
||||
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
|
||||
try:
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
except Exception as e:
|
||||
return random.choice(lameJokes)
|
||||
|
||||
|
||||
250
modules/games/tictactoe.py
Normal file
250
modules/games/tictactoe.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# Tic-Tac-Toe game for Meshtastic mesh-bot
|
||||
# Board positions chosen by numbers 1-9
|
||||
# 2025
|
||||
from modules.log import *
|
||||
import random
|
||||
# to molly and jake, I miss you both so much.
|
||||
|
||||
if disable_emojis_in_games:
|
||||
X = "X"
|
||||
O = "O"
|
||||
else:
|
||||
X = "❌"
|
||||
O = "⭕️"
|
||||
|
||||
class TicTacToe:
|
||||
def __init__(self):
|
||||
self.game = {}
|
||||
|
||||
def new_game(self, id):
|
||||
positiveThoughts = ["🚀I need to call NATO",
|
||||
"🏅Going for the gold!",
|
||||
"Mastering ❌TTT⭕️",]
|
||||
sorryNotGoinWell = ["😭Not your day, huh?",
|
||||
"📉Results here dont define you.",
|
||||
"🤖WOPR would be proud."]
|
||||
"""Start a new game"""
|
||||
games = won = 0
|
||||
ret = ""
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
if games > 0:
|
||||
if won / games >= 3.14159265358979323846: # win rate > pi
|
||||
ret += random.choice(positiveThoughts) + "\n"
|
||||
else:
|
||||
ret += random.choice(sorryNotGoinWell) + "\n"
|
||||
# Retain stats
|
||||
ret += f"Games:{games} 🥇❌:{won}\n"
|
||||
|
||||
self.game[id] = {
|
||||
"board": [" "] * 9, # 3x3 board as flat list
|
||||
"player": X, # Human is X, bot is O
|
||||
"games": games + 1,
|
||||
"won": won,
|
||||
"turn": "human" # whose turn it is
|
||||
}
|
||||
ret += self.show_board(id)
|
||||
ret += "Pick 1-9:"
|
||||
return ret
|
||||
|
||||
def show_board(self, id):
|
||||
"""Display compact board with move numbers"""
|
||||
g = self.game[id]
|
||||
b = g["board"]
|
||||
|
||||
# Show board with positions
|
||||
board_str = ""
|
||||
for i in range(3):
|
||||
row = ""
|
||||
for j in range(3):
|
||||
pos = i * 3 + j
|
||||
if disable_emojis_in_games:
|
||||
cell = b[pos] if b[pos] != " " else str(pos + 1)
|
||||
else:
|
||||
cell = b[pos] if b[pos] != " " else f" {str(pos + 1)} "
|
||||
row += cell
|
||||
if j < 2:
|
||||
row += " | "
|
||||
board_str += row
|
||||
if i < 2:
|
||||
#board_str += "\n-+-+-\n"
|
||||
board_str += "\n"
|
||||
|
||||
return board_str + "\n"
|
||||
|
||||
def make_move(self, id, position):
|
||||
"""Make a move for the current player"""
|
||||
g = self.game[id]
|
||||
|
||||
# Validate position
|
||||
if position < 1 or position > 9:
|
||||
return False
|
||||
|
||||
pos = position - 1
|
||||
if g["board"][pos] != " ":
|
||||
return False
|
||||
|
||||
# Make human move
|
||||
g["board"][pos] = X
|
||||
return True
|
||||
|
||||
def bot_move(self, id):
|
||||
"""AI makes a move"""
|
||||
g = self.game[id]
|
||||
|
||||
# Simple AI: Try to win, block, or pick random
|
||||
move = self.find_winning_move(id, O) # Try to win
|
||||
if move == -1:
|
||||
move = self.find_winning_move(id, X) # Block player
|
||||
if move == -1:
|
||||
move = self.find_random_move(id) # Random move
|
||||
|
||||
if move != -1:
|
||||
g["board"][move] = O
|
||||
return move
|
||||
|
||||
def find_winning_move(self, id, player):
|
||||
"""Find a winning move for the given player"""
|
||||
g = self.game[id]
|
||||
board = g["board"][:]
|
||||
|
||||
# Check all empty positions
|
||||
for i in range(9):
|
||||
if board[i] == " ":
|
||||
board[i] = player
|
||||
if self.check_winner_on_board(board) == player:
|
||||
return i
|
||||
board[i] = " "
|
||||
return -1
|
||||
|
||||
def find_random_move(self, id):
|
||||
"""Find a random empty position"""
|
||||
g = self.game[id]
|
||||
empty = [i for i in range(9) if g["board"][i] == " "]
|
||||
return random.choice(empty) if empty else -1
|
||||
|
||||
def check_winner_on_board(self, board):
|
||||
"""Check winner on given board state"""
|
||||
# Winning combinations
|
||||
wins = [
|
||||
[0,1,2], [3,4,5], [6,7,8], # Rows
|
||||
[0,3,6], [1,4,7], [2,5,8], # Columns
|
||||
[0,4,8], [2,4,6] # Diagonals
|
||||
]
|
||||
|
||||
for combo in wins:
|
||||
if board[combo[0]] == board[combo[1]] == board[combo[2]] != " ":
|
||||
return board[combo[0]]
|
||||
return None
|
||||
|
||||
def check_winner(self, id):
|
||||
"""Check if there's a winner"""
|
||||
g = self.game[id]
|
||||
return self.check_winner_on_board(g["board"])
|
||||
|
||||
def is_board_full(self, id):
|
||||
"""Check if board is full"""
|
||||
g = self.game[id]
|
||||
return " " not in g["board"]
|
||||
|
||||
def game_over_msg(self, id):
|
||||
"""Generate game over message"""
|
||||
g = self.game[id]
|
||||
winner = self.check_winner(id)
|
||||
|
||||
if winner == X:
|
||||
g["won"] += 1
|
||||
return "🎉You won! (n)ew (e)nd"
|
||||
elif winner == X:
|
||||
return "🤖Bot wins! (n)ew (e)nd"
|
||||
else:
|
||||
return "🤝Tie, The only winning move! (n)ew (e)nd"
|
||||
|
||||
def play(self, id, input_msg):
|
||||
"""Main game play function"""
|
||||
if id not in self.game:
|
||||
return self.new_game(id)
|
||||
|
||||
# If input is just "tictactoe", show current board
|
||||
if input_msg.lower().strip() == ("tictactoe" or "tic-tac-toe"):
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
|
||||
g = self.game[id]
|
||||
|
||||
# Parse player move
|
||||
try:
|
||||
# Extract just the number from the input
|
||||
numbers = [char for char in input_msg if char.isdigit()]
|
||||
if not numbers:
|
||||
if input_msg.lower().startswith('q'):
|
||||
self.end_game(id)
|
||||
return "Game ended. To start a new game, type 'tictactoe'."
|
||||
elif input_msg.lower().startswith('n'):
|
||||
return self.new_game(id)
|
||||
elif input_msg.lower().startswith('b'):
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
position = int(numbers[0])
|
||||
except (ValueError, IndexError):
|
||||
return "Enter 1-9, or (e)nd (n)ew game, send (b)oard to see board🧩"
|
||||
|
||||
# Make player move
|
||||
if not self.make_move(id, position):
|
||||
return "Invalid move! Pick 1-9:"
|
||||
|
||||
# Check if player won
|
||||
if self.check_winner(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Check for tie
|
||||
if self.is_board_full(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Bot's turn
|
||||
bot_pos = self.bot_move(id)
|
||||
|
||||
# Check if bot won
|
||||
if self.check_winner(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Check for tie after bot move
|
||||
if self.is_board_full(id):
|
||||
result = self.game_over_msg(id) + "\n" + self.show_board(id)
|
||||
self.end_game(id)
|
||||
return result
|
||||
|
||||
# Continue game
|
||||
return self.show_board(id) + "Your turn! Pick 1-9:"
|
||||
|
||||
def end_game(self, id):
|
||||
"""Clean up finished game but keep stats"""
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
# Remove game but we'll create new one on next play
|
||||
del self.game[id]
|
||||
# Preserve stats for next game
|
||||
self.game[id] = {
|
||||
"board": [" "] * 9,
|
||||
"player": X,
|
||||
"games": games,
|
||||
"won": won,
|
||||
"turn": "human"
|
||||
}
|
||||
|
||||
|
||||
def end(self, id):
|
||||
"""End game completely (called by 'end' command)"""
|
||||
if id in self.game:
|
||||
del self.game[id]
|
||||
|
||||
|
||||
# Global instances for the bot system
|
||||
tictactoeTracker = []
|
||||
tictactoe = TicTacToe()
|
||||
@@ -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
|
||||
|
||||
@@ -10,8 +10,9 @@ 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(shortAlerts=False):
|
||||
def get_govUK_alerts(lat, lon):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
@@ -27,7 +28,23 @@ def get_govUK_alerts(shortAlerts=False):
|
||||
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
|
||||
@@ -57,4 +74,4 @@ def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
return NO_ALERTS
|
||||
|
||||
172
modules/llm.py
172
modules/llm.py
@@ -8,32 +8,34 @@ from modules.log import *
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
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)
|
||||
if not rawLLMQuery:
|
||||
# this may be removed in the future
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# LLM System Variables
|
||||
ollamaAPI = ollamaHostName + "/api/generate"
|
||||
tokens = 450 # max charcters for the LLM response, this is the max length of the response also in prompts
|
||||
requestTruncation = True # if True, the LLM "will" truncate the response
|
||||
|
||||
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
|
||||
|
||||
# Used in the meshBotAI template
|
||||
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
|
||||
antiFloodLLM = []
|
||||
llmChat_history = {}
|
||||
trap_list_llm = ("ask:", "askai")
|
||||
|
||||
meshbotAIinit = """
|
||||
keep responses as short as possible. chatbot assistant no followuyp questions, no asking for clarification.
|
||||
You must respond in plain text standard ASCII characters or emojis.
|
||||
"""
|
||||
|
||||
truncatePrompt = f"truncate this as short as possible:\n"
|
||||
|
||||
meshBotAI = """
|
||||
FROM {llmModel}
|
||||
SYSTEM
|
||||
@@ -74,78 +76,24 @@ if llmEnableHistory:
|
||||
|
||||
"""
|
||||
|
||||
def llm_readTextFiles():
|
||||
# read .txt files in ../data/rag
|
||||
try:
|
||||
text = []
|
||||
directory = "../data/rag"
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith(".txt"):
|
||||
filepath = os.path.join(directory, filename)
|
||||
with open(filepath, 'r') as f:
|
||||
text.append(f.read())
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM readTextFiles: {e}")
|
||||
return False
|
||||
|
||||
def store_text_embedding(text):
|
||||
try:
|
||||
# store each document in a vector embedding database
|
||||
for i, d in enumerate(text):
|
||||
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
|
||||
embedding = response["embedding"]
|
||||
collection.add(
|
||||
ids=[str(i)],
|
||||
embeddings=[embedding],
|
||||
documents=[d]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Embedding failed: {e}")
|
||||
return False
|
||||
|
||||
## INITALIZATION of RAG
|
||||
if ragDEV:
|
||||
try:
|
||||
chromaHostname = "localhost:8000"
|
||||
# connect to the chromaDB
|
||||
chromaHost = chromaHostname.split(":")[0]
|
||||
chromaPort = chromaHostname.split(":")[1]
|
||||
if chromaHost == "localhost" and chromaPort == "8000":
|
||||
# create a client using local python Client
|
||||
chromaClient = chromadb.Client()
|
||||
else:
|
||||
# create a client using the remote python Client
|
||||
# this isnt tested yet please test and report back
|
||||
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
|
||||
|
||||
clearCollection = False
|
||||
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
|
||||
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
|
||||
chromaClient.delete_collection("meshBotAI")
|
||||
|
||||
# create a new collection
|
||||
collection = chromaClient.create_collection("meshBotAI")
|
||||
|
||||
logger.debug(f"System: LLM: Cataloging RAG data")
|
||||
store_text_embedding(llm_readTextFiles())
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
|
||||
|
||||
def query_collection(prompt):
|
||||
# generate an embedding for the prompt and retrieve the most relevant doc
|
||||
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
|
||||
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
|
||||
data = results['documents'][0][0]
|
||||
return data
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
|
||||
# if this is the first initialization of the LLM the query of " " should bring meshbotAIinit OTA shouldnt reach this?
|
||||
# This is for LLM like gemma and others now?
|
||||
if input == " " and rawLLMQuery:
|
||||
logger.warning("System: These LLM models lack a traditional system prompt, they can be verbose and not very helpful be advised.")
|
||||
input = meshbotAIinit
|
||||
|
||||
if not location_name:
|
||||
location_name = "no location provided "
|
||||
|
||||
# remove askai: and ask: from the input
|
||||
for trap in trap_list_llm:
|
||||
if input.lower().startswith(trap):
|
||||
input = input[len(trap):].strip()
|
||||
break
|
||||
|
||||
# add the naughty list here to stop the function before we continue
|
||||
# add a list of allowed nodes only to use the function
|
||||
@@ -156,7 +104,7 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
else:
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
if llmContext_fromGoogle and not rawLLMQuery:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
|
||||
@@ -187,32 +135,29 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
|
||||
|
||||
try:
|
||||
# RAG context inclusion testing
|
||||
ragContext = False
|
||||
if ragDEV:
|
||||
ragContext = query_collection(input)
|
||||
|
||||
if ragContext:
|
||||
ragContextGooogle = ragContext + '\n'.join(googleResults)
|
||||
# Build the query from the template
|
||||
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")
|
||||
if rawLLMQuery:
|
||||
# sanitize the input to remove tool call syntax
|
||||
if '```' in input:
|
||||
logger.warning("System: LLM Query: Code markdown detected, removing for raw query")
|
||||
input = input.replace('```bash', '').replace('```python', '').replace('```', '')
|
||||
modelPrompt = input
|
||||
else:
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
|
||||
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", "")
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
|
||||
# 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:
|
||||
@@ -221,6 +166,23 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
if rawLLMQuery and requestTruncation and len(response) > 450:
|
||||
#retryy loop to truncate the response
|
||||
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
|
||||
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
|
||||
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
|
||||
if truncateResult.status_code == 200:
|
||||
truncate_json = truncateResult.json()
|
||||
result = truncate_json.get("response", "")
|
||||
|
||||
else:
|
||||
#use the original result if truncation fails
|
||||
logger.warning("System: LLM Query: Truncation failed, using original response")
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
# done with the query, remove the user from the anti flood list
|
||||
antiFloodLLM.remove(nodeID)
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
import math
|
||||
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
|
||||
|
||||
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -71,12 +73,24 @@ 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)
|
||||
user_agent = {'User-agent': 'Mozilla/5.0'}
|
||||
response = requests.get(repeater_url, headers=user_agent, timeout=urlTimeoutSeconds)
|
||||
if response.status_code!=200:
|
||||
logger.error(f"Location:Error fetching repeater data from {repeater_url} with status code {response.status_code}")
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
table = soup.find('table', attrs={'class': 'w3-table w3-striped w3-responsive w3-mobile w3-auto sortable'})
|
||||
table = soup.find('table', attrs={'class': 'table table-striped table-hover align-middle sortable'})
|
||||
if table is not None:
|
||||
cells = table.find_all('td')
|
||||
data = []
|
||||
@@ -95,10 +109,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
|
||||
@@ -120,6 +132,8 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
try:
|
||||
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
|
||||
response = requests.get(artsci_url)
|
||||
if response.status_code!=200:
|
||||
logger.error(f"Location:Error fetching data from {artsci_url} with status code {response.status_code}")
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
# results needed xpath is /html/body/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table
|
||||
table = soup.find_all('table')[1]
|
||||
@@ -227,38 +241,39 @@ def get_NOAAweather(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_noaa(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
|
||||
@@ -280,20 +295,13 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
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": "Sun ",
|
||||
"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",
|
||||
@@ -323,6 +331,9 @@ def abbreviate_noaa(row):
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
}
|
||||
|
||||
line = row
|
||||
@@ -393,6 +404,12 @@ def alertBrodcastNOAA():
|
||||
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] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
@@ -443,7 +460,7 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = alerts.split("\n***\n")[:numWxAlerts]
|
||||
|
||||
if alerts == "" or alerts == ['']:
|
||||
return ERROR_FETCHING_DATA
|
||||
return NO_ALERTS
|
||||
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
@@ -457,12 +474,11 @@ 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:
|
||||
@@ -480,23 +496,49 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# extract alerts from main feed
|
||||
for entry in alertxml.getElementsByTagName("entry"):
|
||||
link = entry.getElementsByTagName("link")[0].getAttribute("href")
|
||||
|
||||
## state FIPS
|
||||
## This logic is being added to reduce load on FEMA server.
|
||||
stateFips = None
|
||||
for cat in entry.getElementsByTagName("category"):
|
||||
if cat.getAttribute("label") == "statefips":
|
||||
stateFips = cat.getAttribute("term")
|
||||
break
|
||||
|
||||
if stateFips is None:
|
||||
# no stateFIPS found — skip
|
||||
continue
|
||||
|
||||
# check if it matches your list
|
||||
if stateFips not in myStateFIPSList:
|
||||
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {myStateFIPSList}")
|
||||
continue # skip to next entry
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
# this alert is a full CAP alert
|
||||
linked_xml = xml.dom.minidom.parseString(linked_data.text)
|
||||
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"):
|
||||
# only get en-US language alerts (alternative is es-US)
|
||||
language_nodes = info.getElementsByTagName("language")
|
||||
if not any(node.firstChild and node.firstChild.nodeValue.strip() == "en-US" for node in language_nodes):
|
||||
continue # skip if not en-US
|
||||
# extract values from XML
|
||||
sameVal = "NONE"
|
||||
geocode_value = "NONE"
|
||||
@@ -510,31 +552,35 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
else:
|
||||
logger.debug(f"System: report this to discord - iPAWS No description for alert: {headline}")
|
||||
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):
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
# ignore the FEMA test alerts
|
||||
if ignoreFEMAtest:
|
||||
if "Test" in headline:
|
||||
logger.debug(f"System: Ignoring FEMA Test Alert: {headline} for {areaDesc}")
|
||||
continue
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
# add to alert list
|
||||
alerts.append({
|
||||
'alertType': alertType,
|
||||
'alertCode': alertCode,
|
||||
@@ -544,10 +590,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
'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}")
|
||||
|
||||
else:
|
||||
logger.debug(f"System: iPAWS Alert not in SAME List: {sameVal} or {geocode_value} for {headline} at {areaDesc}")
|
||||
continue
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
for alertItem in alerts[:numWxAlerts]:
|
||||
@@ -612,3 +658,357 @@ def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
|
||||
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
|
||||
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forcast from NWS coastal products
|
||||
try:
|
||||
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pz_data.ok:
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
marine_pz_data = marine_pz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pz_data.startswith("Expires:"):
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
marine_pzz_lines = marine_pz_data.split("\n")
|
||||
marine_pz_report = ""
|
||||
day_blocks = []
|
||||
current_block = ""
|
||||
in_forecast = False
|
||||
|
||||
for line in marine_pzz_lines:
|
||||
if line.startswith(".") and "..." in line:
|
||||
in_forecast = True
|
||||
if current_block:
|
||||
day_blocks.append(current_block.strip())
|
||||
current_block = ""
|
||||
current_block += line.strip() + " "
|
||||
elif in_forecast and line.strip() != "":
|
||||
current_block += line.strip() + " "
|
||||
if current_block:
|
||||
day_blocks.append(current_block.strip())
|
||||
|
||||
# Only keep up to pzDays blocks
|
||||
for block in day_blocks[:days]:
|
||||
marine_pz_report += block + "\n"
|
||||
|
||||
# remove last newline
|
||||
if marine_pz_report.endswith("\n"):
|
||||
marine_pz_report = marine_pz_report[:-1]
|
||||
|
||||
# remove NOAA EOF $$
|
||||
if marine_pz_report.endswith("$$"):
|
||||
marine_pz_report = marine_pz_report[:-2].strip()
|
||||
|
||||
# abbreviate the report
|
||||
marine_pz_report = abbreviate_noaa(marine_pz_report)
|
||||
if marine_pz_report == "":
|
||||
return NO_DATA_NOGPS
|
||||
return marine_pz_report
|
||||
|
||||
def checkUSGSEarthQuake(lat=0, lon=0):
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
radius = 100 # km
|
||||
magnitude = 1.5
|
||||
history = 7 # days
|
||||
startDate = datetime.fromtimestamp(datetime.now().timestamp() - history*24*60*60).strftime("%Y-%m-%d")
|
||||
USGSquake_url = f"https://earthquake.usgs.gov/fdsnws/event/1/query?&format=xml&latitude={lat}&longitude={lon}&maxradiuskm={radius}&minmagnitude={magnitude}&starttime={startDate}"
|
||||
description_text = ""
|
||||
quake_count = 0
|
||||
# fetch the earthquake data from USGS
|
||||
try:
|
||||
quake_data = requests.get(USGSquake_url, timeout=urlTimeoutSeconds)
|
||||
if not quake_data.ok:
|
||||
logger.warning("Location:Error fetching earthquake data from USGS")
|
||||
return NO_ALERTS
|
||||
if not quake_data.text.strip():
|
||||
return NO_ALERTS
|
||||
try:
|
||||
quake_xml = xml.dom.minidom.parseString(quake_data.text)
|
||||
except Exception as e:
|
||||
logger.warning(f"Location: USGS earthquake API returned invalid XML: {e}")
|
||||
return NO_ALERTS
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching earthquake data from USGS")
|
||||
return NO_ALERTS
|
||||
|
||||
quake_xml = xml.dom.minidom.parseString(quake_data.text)
|
||||
quake_count = len(quake_xml.getElementsByTagName("event"))
|
||||
|
||||
#get largest mag in magnitude of the set of quakes
|
||||
largest_mag = 0.0
|
||||
for event in quake_xml.getElementsByTagName("event"):
|
||||
mag = event.getElementsByTagName("magnitude")[0]
|
||||
mag_value = float(mag.getElementsByTagName("value")[0].childNodes[0].nodeValue)
|
||||
if mag_value > largest_mag:
|
||||
largest_mag = mag_value
|
||||
# set description text
|
||||
description_text = event.getElementsByTagName("description")[0].getElementsByTagName("text")[0].childNodes[0].nodeValue
|
||||
largest_mag = round(largest_mag, 1)
|
||||
if quake_count == 0:
|
||||
return NO_ALERTS
|
||||
else:
|
||||
return f"{quake_count} 🫨quakes in last {history} days within {radius} km. Largest: {largest_mag}M\n{description_text}"
|
||||
|
||||
|
||||
howfarDB = {}
|
||||
def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
# part of the howfar function, calculates the distance between two lat/lon points
|
||||
msg = ""
|
||||
dupe = False
|
||||
r = 6371 # Radius of earth in kilometers # haversine formula
|
||||
|
||||
if lat == 0 and lon == 0:
|
||||
return NO_DATA_NOGPS
|
||||
if nodeID == 0:
|
||||
return "No NodeID provided"
|
||||
|
||||
if reset:
|
||||
if nodeID in howfarDB:
|
||||
del howfarDB[nodeID]
|
||||
|
||||
if nodeID not in howfarDB:
|
||||
#register first point NodeID, lat, lon, time, point
|
||||
howfarDB[nodeID] = [{'lat': lat, 'lon': lon, 'time': datetime.now()}]
|
||||
if reset:
|
||||
return "Tracking reset, new starting point registered🗺️"
|
||||
else:
|
||||
return "Starting point registered🗺️"
|
||||
else:
|
||||
#de-dupe points if same as last point
|
||||
if howfarDB[nodeID][-1]['lat'] == lat and howfarDB[nodeID][-1]['lon'] == lon:
|
||||
dupe = True
|
||||
msg = "No New GPS📍 "
|
||||
# calculate distance from last point in howfarDB
|
||||
last_point = howfarDB[nodeID][-1]
|
||||
lat1 = math.radians(last_point['lat'])
|
||||
lon1 = math.radians(last_point['lon'])
|
||||
lat2 = math.radians(lat)
|
||||
lon2 = math.radians(lon)
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
|
||||
c = 2 * math.asin(math.sqrt(a))
|
||||
|
||||
distance_km = c * r
|
||||
if use_metric:
|
||||
msg += f"{distance_km:.2f} km"
|
||||
else:
|
||||
distance_miles = distance_km * 0.621371
|
||||
msg += f"{distance_miles:.2f} miles"
|
||||
|
||||
#calculate bearing
|
||||
x = math.sin(dlon) * math.cos(lat2)
|
||||
y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(dlon))
|
||||
initial_bearing = math.atan2(x, y)
|
||||
initial_bearing = math.degrees(initial_bearing)
|
||||
compass_bearing = (initial_bearing + 360) % 360
|
||||
msg += f" 🧭{compass_bearing:.2f}° Bearing from last📍"
|
||||
|
||||
# calculate the speed if time difference is more than 1 minute
|
||||
time_diff = datetime.now() - last_point['time']
|
||||
if time_diff.total_seconds() > 60:
|
||||
hours = time_diff.total_seconds() / 3600
|
||||
if use_metric:
|
||||
speed = distance_km / hours
|
||||
speed_str = f"{speed:.2f} km/h"
|
||||
else:
|
||||
speed_mph = (distance_km * 0.621371) / hours
|
||||
speed_str = f"{speed_mph:.2f} mph"
|
||||
msg += f", travel time: {int(time_diff.total_seconds()//60)} min, Speed: {speed_str}"
|
||||
|
||||
# calculate total distance traveled including this point computed in distance_km from calculate distance from last point in howfarDB
|
||||
total_distance_km = 0.0
|
||||
for i in range(1, len(howfarDB[nodeID])):
|
||||
point1 = howfarDB[nodeID][i-1]
|
||||
point2 = howfarDB[nodeID][i]
|
||||
lat1 = math.radians(point1['lat'])
|
||||
lon1 = math.radians(point1['lon'])
|
||||
lat2 = math.radians(point2['lat'])
|
||||
lon2 = math.radians(point2['lon'])
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
|
||||
c = 2 * math.asin(math.sqrt(a))
|
||||
total_distance_km += c * r
|
||||
# add the distance from last point to current point
|
||||
total_distance_km += distance_km
|
||||
if use_metric:
|
||||
msg += f", Total: {total_distance_km:.2f} km"
|
||||
else:
|
||||
total_distance_miles = total_distance_km * 0.621371
|
||||
msg += f", Total: {total_distance_miles:.2f} miles"
|
||||
|
||||
# update the last point in howfarDB
|
||||
if not dupe:
|
||||
howfarDB[nodeID].append({'lat': lat, 'lon': lon, 'time': datetime.now()})
|
||||
|
||||
# if points 3+ are within 30 meters of the first point add the area of the polygon
|
||||
if len(howfarDB[nodeID]) >= 3:
|
||||
points = []
|
||||
# loop the howfarDB to get all the points except the current nodeID
|
||||
for key in howfarDB:
|
||||
if key != nodeID:
|
||||
points.append((howfarDB[key][-1]['lat'], howfarDB[key][-1]['lon']))
|
||||
# loop the howfarDB[nodeID] to get the points
|
||||
for point in howfarDB[nodeID]:
|
||||
points.append((point['lat'], point['lon']))
|
||||
# close the polygon by adding the first point to the end
|
||||
points.append((howfarDB[nodeID][0]['lat'], howfarDB[nodeID][0]['lon']))
|
||||
# calculate the area of the polygon
|
||||
area = 0.0
|
||||
for i in range(len(points)-1):
|
||||
lat1 = math.radians(points[i][0])
|
||||
lon1 = math.radians(points[i][1])
|
||||
lat2 = math.radians(points[i+1][0])
|
||||
lon2 = math.radians(points[i+1][1])
|
||||
area += (lon2 - lon1) * (2 + math.sin(lat1) + math.sin(lat2))
|
||||
area = area * (6378137 ** 2) / 2.0
|
||||
area = abs(area) / 1e6 # convert to square kilometers
|
||||
|
||||
if use_metric:
|
||||
msg += f", Area: {area:.2f} sq.km (approx)"
|
||||
else:
|
||||
area_miles = area * 0.386102
|
||||
msg += f", Area: {area_miles:.2f} sq.mi (approx)"
|
||||
|
||||
#calculate the centroid of the polygon
|
||||
x = 0.0
|
||||
y = 0.0
|
||||
z = 0.0
|
||||
for point in points[:-1]:
|
||||
lat_rad = math.radians(point[0])
|
||||
lon_rad = math.radians(point[1])
|
||||
x += math.cos(lat_rad) * math.cos(lon_rad)
|
||||
y += math.cos(lat_rad) * math.sin(lon_rad)
|
||||
z += math.sin(lat_rad)
|
||||
total_points = len(points) - 1
|
||||
x /= total_points
|
||||
y /= total_points
|
||||
z /= total_points
|
||||
lon_centroid = math.atan2(y, x)
|
||||
hyp = math.sqrt(x * x + y * y)
|
||||
lat_centroid = math.atan2(z, hyp)
|
||||
lat_centroid = math.degrees(lat_centroid)
|
||||
lon_centroid = math.degrees(lon_centroid)
|
||||
msg += f", Centroid: {lat_centroid:.5f}, {lon_centroid:.5f}"
|
||||
|
||||
return msg
|
||||
|
||||
def get_openskynetwork(lat=0, lon=0):
|
||||
# get the latest aircraft data from OpenSky Network in the area
|
||||
if lat == 0 and lon == 0:
|
||||
return NO_ALERTS
|
||||
# setup a bounding box of 50km around the lat/lon
|
||||
box_size = 0.45 # approx 50km
|
||||
# return limits for aircraft search
|
||||
search_limit = 3
|
||||
lamin = lat - box_size
|
||||
lamax = lat + box_size
|
||||
lomin = lon - box_size
|
||||
lomax = lon + box_size
|
||||
|
||||
# fetch the aircraft data from OpenSky Network
|
||||
opensky_url = f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}&lamax={lamax}&lomax={lomax}"
|
||||
try:
|
||||
aircraft_data = requests.get(opensky_url, timeout=urlTimeoutSeconds)
|
||||
if not aircraft_data.ok:
|
||||
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
|
||||
return ERROR_FETCHING_DATA
|
||||
aircraft_json = aircraft_data.json()
|
||||
if 'states' not in aircraft_json or not aircraft_json['states']:
|
||||
return NO_ALERTS
|
||||
aircraft_list = aircraft_json['states']
|
||||
aircraft_report = ""
|
||||
for aircraft in aircraft_list:
|
||||
if len(aircraft_report.split("\n")) >= search_limit:
|
||||
break
|
||||
# extract values from JSON
|
||||
try:
|
||||
callsign = aircraft[1].strip() if aircraft[1] else "N/A"
|
||||
origin_country = aircraft[2]
|
||||
velocity = aircraft[9]
|
||||
true_track = aircraft[10]
|
||||
vertical_rate = aircraft[11]
|
||||
sensors = aircraft[12]
|
||||
geo_altitude = aircraft[13]
|
||||
squawk = aircraft[14] if len(aircraft) > 14 else "N/A"
|
||||
except Exception as e:
|
||||
logger.debug("Location:Error extracting aircraft data from OpenSky Network")
|
||||
continue
|
||||
|
||||
# format the aircraft data
|
||||
aircraft_report += f"{callsign} Alt:{int(geo_altitude) if geo_altitude else 'N/A'}m Vel:{int(velocity) if velocity else 'N/A'}m/s Heading:{int(true_track) if true_track else 'N/A'}°\n"
|
||||
|
||||
# remove last newline
|
||||
if aircraft_report.endswith("\n"):
|
||||
aircraft_report = aircraft_report[:-1]
|
||||
aircraft_report = abbreviate_noaa(aircraft_report)
|
||||
return aircraft_report if aircraft_report else NO_ALERTS
|
||||
|
||||
@@ -69,32 +69,28 @@ 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 = 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)
|
||||
|
||||
# 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
|
||||
# convert unix time to minutes, hours, days, or years for simple display
|
||||
if seconds < 60:
|
||||
return f"{int(seconds)}s"
|
||||
elif seconds < 3600:
|
||||
return f"{int(round(seconds / 60))}m"
|
||||
elif seconds < 86400:
|
||||
return f"{int(round(seconds / 3600))}h"
|
||||
elif seconds < 31536000:
|
||||
return f"{int(round(seconds / 86400))}d"
|
||||
else:
|
||||
return f"{int(round(seconds / 31536000))}y"
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,10 @@ ping_enabled = True # ping feature to respond to pings, ack's etc.
|
||||
sitrep_enabled = True # sitrep feature to respond to sitreps
|
||||
lastHamLibAlert = 0 # last alert from hamlib
|
||||
lastFileAlert = 0 # last alert from file monitor
|
||||
max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 4 # default retry count for interfaces
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
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
|
||||
@@ -36,9 +34,13 @@ 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}")
|
||||
# exit if we can't read the config file
|
||||
print(f"System: Check the config.ini against config.template file for missing sections or values.")
|
||||
print(f"System: Exiting...")
|
||||
exit(1)
|
||||
|
||||
if config.sections() == []:
|
||||
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
|
||||
@@ -91,6 +93,14 @@ 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', '')
|
||||
@@ -188,7 +198,10 @@ 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
|
||||
explicitCmd = config['general'].getboolean('explicitCmd', True) # default on
|
||||
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
|
||||
@@ -211,8 +224,15 @@ try:
|
||||
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
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma3:270m') # default gemma3:270m
|
||||
rawLLMQuery = config['general'].getboolean('rawLLMQuery', True) #default True
|
||||
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True) # default True
|
||||
dont_retry_disconnect = config['general'].getboolean('dont_retry_disconnect', False) # default False, retry on disconnect
|
||||
favoriteNodeList = config['general'].get('favoriteNodeList', '').split(',')
|
||||
enableEcho = config['general'].getboolean('enableEcho', False) # default False
|
||||
echoChannel = config['general'].getint('echoChannel', '9') # default 9, empty string to ignore
|
||||
|
||||
# emergency response
|
||||
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
|
||||
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
|
||||
@@ -222,10 +242,18 @@ try:
|
||||
# sentry
|
||||
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
|
||||
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
|
||||
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
|
||||
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
|
||||
highfly_interface = config['sentry'].getint('highFlyingAlertInterface', 1) # default 1
|
||||
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
|
||||
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
|
||||
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -236,20 +264,33 @@ try:
|
||||
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
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default None
|
||||
coastalEnabled = config['location'].getboolean('coastalEnabled', False) # default False
|
||||
myCoastalZone = config['location'].get('myCoastalZone', None) # default None
|
||||
coastalForecastDays = config['location'].getint('coastalForecastDays', 3) # default 3 days
|
||||
|
||||
# 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
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
|
||||
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
|
||||
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
|
||||
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
myStateFIPSList = config['location'].get('myFIPSList', '').split(',') # default empty
|
||||
mySAMEList = config['location'].get('mySAMEList', '').split(',') # default empty
|
||||
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)
|
||||
@@ -258,7 +299,19 @@ try:
|
||||
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(',')
|
||||
bbsAPI_enabled = config['bbs'].getboolean('bbsAPI_enabled', False)
|
||||
|
||||
# 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)
|
||||
@@ -282,6 +335,12 @@ try:
|
||||
|
||||
# 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)
|
||||
@@ -295,29 +354,39 @@ try:
|
||||
# file monitor
|
||||
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
|
||||
allowXcmd = config['fileMon'].getboolean('allowXcmd', False) # default False
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
game_hop_limit = config['games'].getint('game_hop_limit', 5) # default 5 hops
|
||||
disable_emojis_in_games = config['games'].getboolean('disable_emojis', False) # default False
|
||||
dopewars_enabled = config['games'].getboolean('dopeWars', True)
|
||||
lemonade_enabled = config['games'].getboolean('lemonade', True)
|
||||
blackjack_enabled = config['games'].getboolean('blackjack', True)
|
||||
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)
|
||||
tictactoe_enabled = config['games'].getboolean('tictactoe', 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
|
||||
|
||||
except KeyError as e:
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
|
||||
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
|
||||
DEBUGpacket = config['messagingSettings'].getboolean('DEBUGpacket', False) # default False
|
||||
noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False
|
||||
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
|
||||
except Exception as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
print(f"System: Check the config.ini against config.template file for missing sections or values.")
|
||||
print(f"System: Exiting...")
|
||||
|
||||
@@ -152,6 +152,12 @@ def store_sms(nodeID, sms):
|
||||
global sms_db
|
||||
try:
|
||||
logger.debug("System: Setting SMS for " + str(nodeID))
|
||||
# if the nodeID has over 5 sms addresses warn and return
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
if len(item['sms']) >= 5:
|
||||
logger.warning("System: 📵SMS limit reached for " + str(nodeID))
|
||||
return False
|
||||
# if not in db, add it
|
||||
if nodeID not in sms_db:
|
||||
sms_db.append({'nodeID': nodeID, 'sms': sms})
|
||||
|
||||
106
modules/space.py
106
modules/space.py
@@ -6,20 +6,25 @@ import requests # pip install requests
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from modules.log import *
|
||||
import math
|
||||
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass", "howtall")
|
||||
|
||||
def hf_band_conditions():
|
||||
# ham radio HF band conditions
|
||||
hf_cond = ""
|
||||
signalnoise = ""
|
||||
band_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
|
||||
if(band_cond.ok):
|
||||
solarxml = xml.dom.minidom.parseString(band_cond.text)
|
||||
for i in solarxml.getElementsByTagName("band"):
|
||||
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
|
||||
hf_cond = hf_cond[:-1] # remove the last newline
|
||||
for i in solarxml.getElementsByTagName("solardata"):
|
||||
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
|
||||
hf_cond += "\nQRN:" + signalnoise
|
||||
else:
|
||||
logger.error("Solar: Error fetching HF band conditions")
|
||||
hf_cond = ERROR_FETCHING_DATA
|
||||
@@ -63,7 +68,7 @@ def drap_xray_conditions():
|
||||
def get_sun(lat=0, lon=0):
|
||||
# get sunrise and sunset times using callers location or default
|
||||
obs = ephem.Observer()
|
||||
obs.date = datetime.now()
|
||||
obs.date = datetime.now(timezone.utc)
|
||||
sun = ephem.Sun()
|
||||
if lat != 0 and lon != 0:
|
||||
obs.lat = str(lat)
|
||||
@@ -74,9 +79,17 @@ def get_sun(lat=0, lon=0):
|
||||
|
||||
sun.compute(obs)
|
||||
sun_table = {}
|
||||
|
||||
# get the sun azimuth and altitude
|
||||
sun_table['azimuth'] = sun.az
|
||||
sun_table['altitude'] = sun.alt
|
||||
|
||||
# sun is up include altitude
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_table['altitude'] = sun.alt
|
||||
else:
|
||||
sun_table['altitude'] = 0
|
||||
|
||||
# get the next rise and set times
|
||||
local_sunrise = ephem.localtime(obs.next_rising(sun))
|
||||
local_sunset = ephem.localtime(obs.next_setting(sun))
|
||||
@@ -86,19 +99,25 @@ def get_sun(lat=0, lon=0):
|
||||
else:
|
||||
sun_table['rise_time'] = local_sunrise.strftime('%a %d %I:%M%p')
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
|
||||
# if sunset is before sunrise, then it's tomorrow
|
||||
|
||||
# if sunset is before sunrise, then data will be for tomorrow format sunset first and sunrise second
|
||||
if local_sunset < local_sunrise:
|
||||
local_sunset = ephem.localtime(obs.next_setting(sun)) + timedelta(1)
|
||||
if zuluTime:
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
|
||||
else:
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
|
||||
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
|
||||
sun_data = "SunSet: " + sun_table['set_time'] + "\nRise: " + sun_table['rise_time']
|
||||
else:
|
||||
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
|
||||
|
||||
sun_data += "\nDaylight: " + str((local_sunset - local_sunrise).seconds // 3600) + "h " + str(((local_sunset - local_sunrise).seconds // 60) % 60) + "m"
|
||||
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_data += "\nRemaining: " + str((local_sunset - datetime.now()).seconds // 3600) + "h " + str(((local_sunset - datetime.now()).seconds // 60) % 60) + "m"
|
||||
|
||||
sun_data += "\nAzimuth: " + str('{0:.2f}'.format(sun_table['azimuth'] * 180 / ephem.pi)) + "°"
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_data += "\nAltitude: " + str('{0:.2f}'.format(sun_table['altitude'] * 180 / ephem.pi)) + "°"
|
||||
return sun_data
|
||||
|
||||
def get_moon(lat=0, lon=0):
|
||||
# get moon phase and rise/set times using callers location or default
|
||||
# the phase calculation mght not be accurate (followup later)
|
||||
obs = ephem.Observer()
|
||||
moon = ephem.Moon()
|
||||
if lat != 0 and lon != 0:
|
||||
@@ -108,10 +127,28 @@ def get_moon(lat=0, lon=0):
|
||||
obs.lat = str(latitudeValue)
|
||||
obs.lon = str(longitudeValue)
|
||||
|
||||
obs.date = datetime.now()
|
||||
obs.date = datetime.now(timezone.utc)
|
||||
moon.compute(obs)
|
||||
moon_table = {}
|
||||
moon_phase = ['NewMoon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous', 'FullMoon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent'][round(moon.phase / (2 * ephem.pi) * 8) % 8]
|
||||
illum = moon.phase # 0 = new, 50 = first/last quarter, 100 = full
|
||||
|
||||
if illum < 1.0:
|
||||
moon_phase = 'New Moon🌑'
|
||||
elif illum < 49:
|
||||
moon_phase = 'Waxing Crescent 🌒'
|
||||
elif 49 <= illum < 51:
|
||||
moon_phase = 'First Quarter 🌓'
|
||||
elif illum < 99:
|
||||
moon_phase = 'Waxing Gibbous 🌔'
|
||||
elif illum >= 99:
|
||||
moon_phase = 'Full Moon🌕'
|
||||
elif illum > 51:
|
||||
moon_phase = 'Waning Gibbous 🌖'
|
||||
elif 51 >= illum > 49:
|
||||
moon_phase = 'Last Quarter 🌗'
|
||||
else:
|
||||
moon_phase = 'Waning Crescent 🌘'
|
||||
|
||||
moon_table['phase'] = moon_phase
|
||||
moon_table['illumination'] = moon.phase
|
||||
moon_table['azimuth'] = moon.az
|
||||
@@ -135,9 +172,14 @@ def get_moon(lat=0, lon=0):
|
||||
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %I:%M%p')
|
||||
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %I:%M%p')
|
||||
|
||||
moon_data = "MoonRise:" + moon_table['rise_time'] + "\nSet:" + moon_table['set_time'] + \
|
||||
"\nPhase:" + moon_table['phase'] + " @:" + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
|
||||
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
|
||||
moon_data = "MoonRise: " + moon_table['rise_time'] + "\nSet: " + moon_table['set_time'] + \
|
||||
"\nPhase: " + moon_table['phase'] + " @: " + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
|
||||
+ "\nFullMoon: " + moon_table['next_full_moon'] + "\nNewMoon: " + moon_table['next_new_moon']
|
||||
|
||||
# if moon is in the sky, add azimuth and altitude
|
||||
if moon_table['altitude'] > 0:
|
||||
moon_data += "\nAz: " + str('{0:.2f}'.format(moon_table['azimuth'] * 180 / ephem.pi)) + "°" + \
|
||||
"\nAlt: " + str('{0:.2f}'.format(moon_table['altitude'] * 180 / ephem.pi)) + "°"
|
||||
|
||||
return moon_data
|
||||
|
||||
@@ -169,7 +211,7 @@ def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
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}"
|
||||
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"
|
||||
@@ -178,5 +220,33 @@ def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
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"
|
||||
pass_data = "Provide NORAD# example use: 🛰️satpass 25544,33591"
|
||||
return pass_data
|
||||
|
||||
def measureHeight(lat=0, lon=0, shadow=0):
|
||||
# measure height of a given location using sun angle and shadow length
|
||||
if lat == 0 and lon == 0:
|
||||
return NO_DATA_NOGPS
|
||||
if shadow == 0:
|
||||
return NO_ALERTS
|
||||
obs = ephem.Observer()
|
||||
obs.lat = str(lat)
|
||||
obs.lon = str(lon)
|
||||
obs.date = datetime.now(timezone.utc)
|
||||
sun = ephem.Sun()
|
||||
sun.compute(obs)
|
||||
sun_altitude = sun.alt * 180 / ephem.pi
|
||||
if sun_altitude <= 0:
|
||||
return "☀️Sun is below horizon, I dont belive your shadow measurement"
|
||||
try:
|
||||
if use_metric:
|
||||
height = float(shadow) * math.tan(sun.alt)
|
||||
return f"📏Object Height: {height:.2f} m (Shadow: {shadow} m, 📐Sun Alt: {sun_altitude:.2f}°)"
|
||||
else:
|
||||
# Assume shadow is in feet if imperial, otherwise convert from meters to feet
|
||||
shadow_ft = float(shadow)
|
||||
height_ft = shadow_ft * math.tan(sun.alt)
|
||||
return f"📏Object Height: {height_ft:.2f} ft (Shadow: {shadow_ft} ft, 📐Sun Alt: {sun_altitude:.2f}°)"
|
||||
except Exception as e:
|
||||
logger.error(f"Space: Error calculating height: {e}")
|
||||
return NO_ALERTS
|
||||
@@ -7,17 +7,84 @@ import meshtastic.ble_interface
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
# not ideal but needed?
|
||||
import contextlib # for suppressing output on watchdog
|
||||
import io # for suppressing output on watchdog
|
||||
# homebrew 'modules'
|
||||
from modules.log import *
|
||||
|
||||
# Global Variables
|
||||
debugMetadata = False # packet debug for non text messages
|
||||
trap_list = ("cmd","cmd?") # default trap list
|
||||
help_message = "Bot CMD?:"
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
interface_retry_count = 3
|
||||
|
||||
# Memory Management Constants
|
||||
MAX_MSG_HISTORY = 100
|
||||
MAX_CMD_HISTORY = 200
|
||||
MAX_SEEN_NODES = 200
|
||||
CLEANUP_INTERVAL = 86400 # 24 hours in seconds
|
||||
GAMEDELAY = CLEANUP_INTERVAL # the age of game entries in seconds before they are cleaned up
|
||||
|
||||
def cleanup_memory():
|
||||
"""Clean up memory by limiting list sizes and removing stale entries"""
|
||||
global cmdHistory, seenNodes, multiPingList
|
||||
current_time = time.time()
|
||||
|
||||
try:
|
||||
# Limit cmdHistory size
|
||||
if 'cmdHistory' in globals() and len(cmdHistory) > MAX_CMD_HISTORY:
|
||||
cmdHistory = cmdHistory[-(MAX_CMD_HISTORY - 50):] # keep the most recent 50 entries
|
||||
logger.debug(f"System: Trimmed cmdHistory to {len(cmdHistory)} entries")
|
||||
|
||||
# Clean up old seenNodes entries (older than 24 hours)
|
||||
if 'seenNodes' in globals():
|
||||
initial_count = len(seenNodes)
|
||||
seenNodes = [node for node in seenNodes
|
||||
if current_time - node.get('lastSeen', 0) < 86400]
|
||||
if len(seenNodes) < initial_count:
|
||||
logger.debug(f"System: Cleaned up {initial_count - len(seenNodes)} old seenNodes entries")
|
||||
|
||||
# Clean up stale game tracker entries
|
||||
cleanup_game_trackers(current_time)
|
||||
|
||||
# Clean up multiPingList of completed or stale entries
|
||||
if 'multiPingList' in globals():
|
||||
multiPingList[:] = [ping for ping in multiPingList
|
||||
if ping.get('message_from_id', 0) != 0 and
|
||||
ping.get('count', 0) > 0]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error during memory cleanup: {e}")
|
||||
|
||||
def cleanup_game_trackers(current_time):
|
||||
"""Clean up all game tracker lists of stale entries"""
|
||||
try:
|
||||
# List of game tracker global variable names
|
||||
tracker_names = [
|
||||
'dwPlayerTracker', 'lemonadeTracker', 'jackTracker',
|
||||
'vpTracker', 'mindTracker', 'golfTracker',
|
||||
'hangmanTracker', 'hamtestTracker', 'tictactoeTracker'
|
||||
]
|
||||
|
||||
for tracker_name in tracker_names:
|
||||
if tracker_name in globals():
|
||||
tracker = globals()[tracker_name]
|
||||
if isinstance(tracker, list):
|
||||
initial_count = len(tracker)
|
||||
# Remove entries older than GAMEDELAY
|
||||
globals()[tracker_name] = [
|
||||
entry for entry in tracker
|
||||
if current_time - entry.get('last_played', entry.get('time', 0)) < GAMEDELAY
|
||||
]
|
||||
cleaned_count = initial_count - len(globals()[tracker_name])
|
||||
if cleaned_count > 0:
|
||||
logger.debug(f"System: Cleaned up {cleaned_count} stale entries from {tracker_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error cleaning up game trackers: {e}")
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
@@ -26,6 +93,12 @@ if ping_enabled:
|
||||
trap_list = trap_list + trap_list_ping
|
||||
help_message = help_message + "ping"
|
||||
|
||||
# Echo Configuration
|
||||
if enableEcho:
|
||||
trap_list_echo = ("echo",)
|
||||
trap_list = trap_list + trap_list_echo
|
||||
help_message = help_message + ", echo"
|
||||
|
||||
# Sitrep Configuration
|
||||
if sitrep_enabled:
|
||||
trap_list_sitrep = ("sitrep", "lheard", "sysinfo")
|
||||
@@ -69,23 +142,52 @@ else:
|
||||
if enableCmdHistory:
|
||||
trap_list = trap_list + ("history",)
|
||||
#help_message = help_message + ", history"
|
||||
|
||||
|
||||
# Location Configuration
|
||||
if location_enabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
|
||||
help_message = help_message + ", whereami, wx, wxc, rlist"
|
||||
if enableGBalerts:
|
||||
trap_list = trap_list + trap_list_location
|
||||
help_message = help_message + ", whereami, wx, howfar"
|
||||
if enableGBalerts and not enableDEalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_eu
|
||||
logger.warning(f"System: GB Alerts not functional at this time need to find a source API")
|
||||
#help_message = help_message + ", ukalert, ukwx, ukflood"
|
||||
if enableDEalerts and not enableGBalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_de
|
||||
#help_message = help_message + ", dealert, dewx, deflood"
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
trap_list = trap_list + ("wxc",)
|
||||
help_message = help_message + ", wxc"
|
||||
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
|
||||
else:
|
||||
# NOAA only features
|
||||
help_message = help_message + ", wxa, tide, ealert"
|
||||
help_message = help_message + ", wxa, wxalert"
|
||||
|
||||
# USGS riverFlow Configuration
|
||||
if riverListDefault != ['']:
|
||||
help_message = help_message + ", riverflow"
|
||||
|
||||
if repeater_lookup != False:
|
||||
help_message = help_message + ", rlist"
|
||||
|
||||
if solar_conditions_enabled:
|
||||
help_message = help_message + ", howtall"
|
||||
|
||||
# NOAA alerts needs location module
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
# limited subset, this should be done better but eh..
|
||||
trap_list = trap_list + ("wx", "wxa", "wxalert", "ea", "ealert", "valert")
|
||||
help_message = help_message + ", ealert, valert"
|
||||
|
||||
# NOAA Coastal Waters Forecasts
|
||||
if coastalEnabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("mwx","tide",)
|
||||
help_message = help_message + ", mwx, tide"
|
||||
|
||||
# BBS Configuration
|
||||
if bbs_enabled:
|
||||
@@ -147,7 +249,22 @@ if golfSim_enabled:
|
||||
from modules.games.golfsim import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("golfsim",)
|
||||
games_enabled = True
|
||||
|
||||
|
||||
if hangman_enabled:
|
||||
from modules.games.hangman import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("hangman",)
|
||||
games_enabled = True
|
||||
|
||||
if hamtest_enabled:
|
||||
from modules.games.hamtest import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("hamtest",)
|
||||
games_enabled = True
|
||||
|
||||
if tictactoe_enabled:
|
||||
from modules.games.tictactoe import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("tictactoe","tic-tac-toe",)
|
||||
games_enabled = True
|
||||
|
||||
# Games Configuration
|
||||
if games_enabled is True:
|
||||
help_message = help_message + ", games"
|
||||
@@ -159,7 +276,8 @@ if games_enabled is True:
|
||||
if lemonade_enabled:
|
||||
gamesCmdList += "lemonStand, "
|
||||
if gTnW_enabled:
|
||||
trap_list = trap_list + ("globalthermonuclearwar",)
|
||||
trap_list = trap_list + ("globalthermonuclearwar","chess")
|
||||
gamesCmdList += "chess, "
|
||||
if blackjack_enabled:
|
||||
gamesCmdList += "blackJack, "
|
||||
if videoPoker_enabled:
|
||||
@@ -168,6 +286,12 @@ if games_enabled is True:
|
||||
gamesCmdList += "masterMind, "
|
||||
if golfSim_enabled:
|
||||
gamesCmdList += "golfSim, "
|
||||
if hangman_enabled:
|
||||
gamesCmdList += "hangman, "
|
||||
if hamtest_enabled:
|
||||
gamesCmdList += "hamTest, "
|
||||
if tictactoe_enabled:
|
||||
gamesCmdList += "ticTacToe, "
|
||||
gamesCmdList = gamesCmdList[:-2] # remove the last comma
|
||||
else:
|
||||
gamesCmdList = ""
|
||||
@@ -186,12 +310,24 @@ if store_forward_enabled:
|
||||
trap_list = trap_list + ("messages",)
|
||||
help_message = help_message + ", messages"
|
||||
|
||||
# QRZ Configuration
|
||||
if qrz_hello_enabled:
|
||||
from modules.qrz import * # from the spudgunman/meshing-around repo
|
||||
#trap_list = trap_list + trap_list_qrz # items qrz, qrz?, qrzcall
|
||||
#help_message = help_message + ", qrz"
|
||||
|
||||
# CheckList Configuration
|
||||
if checklist_enabled:
|
||||
from modules.checklist import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_checklist # items checkin, checkout, checklist, purgein, purgeout
|
||||
help_message = help_message + ", checkin, checkout"
|
||||
|
||||
# Radio Monitor Configuration
|
||||
if radio_detection_enabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled or read_news_enabled:
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
@@ -199,6 +335,9 @@ if file_monitor_enabled or read_news_enabled:
|
||||
# Bee Configuration uses file monitor module
|
||||
if bee_enabled:
|
||||
trap_list = trap_list + ("🐝",)
|
||||
# x: command for shell access
|
||||
if enable_runShellCmd and allowXcmd:
|
||||
trap_list = trap_list + ("x:",)
|
||||
|
||||
# clean up the help message
|
||||
help_message = help_message.split(", ")
|
||||
@@ -218,6 +357,8 @@ if ble_count > 1:
|
||||
logger.debug(f"System: Initializing Interfaces")
|
||||
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
|
||||
retry_int1 = retry_int2 = retry_int3 = retry_int4 = retry_int5 = retry_int6 = retry_int7 = retry_int8 = retry_int9 = False
|
||||
myNodeNum1 = myNodeNum2 = myNodeNum3 = myNodeNum4 = myNodeNum5 = myNodeNum6 = myNodeNum7 = myNodeNum8 = myNodeNum9 = 777
|
||||
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = interface_retry_count
|
||||
for i in range(1, 10):
|
||||
interface_type = globals().get(f'interface{i}_type')
|
||||
if not interface_type or interface_type == 'none' or globals().get(f'interface{i}_enabled') == False:
|
||||
@@ -347,27 +488,27 @@ def get_node_list(nodeInt=1):
|
||||
|
||||
return node_list
|
||||
|
||||
def get_node_location(number, nodeInt=1, channel=0):
|
||||
def get_node_location(nodeID, nodeInt=1, channel=0):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the location of a node by its number from nodeDB on device
|
||||
# if no location data, return default location
|
||||
latitude = latitudeValue
|
||||
longitude = longitudeValue
|
||||
position = [latitudeValue,longitudeValue]
|
||||
lastheard = 0
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
if number == node['num']:
|
||||
if 'position' in node:
|
||||
if nodeID == node['num']:
|
||||
if 'position' in node and node['position'] is not {}:
|
||||
try:
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
logger.debug(f"System: location data for {nodeID} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error getting location data for {number}")
|
||||
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
logger.debug(f"System: No location data for {nodeID} use default location")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No location data for {number} using default location")
|
||||
logger.debug(f"System: No location data for {nodeID} using default location")
|
||||
# request location data
|
||||
# try:
|
||||
# logger.debug(f"System: Requesting location data for {number}")
|
||||
@@ -376,7 +517,7 @@ def get_node_location(number, nodeInt=1, channel=0):
|
||||
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No nodes found")
|
||||
logger.warning(f"System: Location for NodeID {nodeID} not found in nodeDb")
|
||||
return position
|
||||
|
||||
|
||||
@@ -423,8 +564,35 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
else:
|
||||
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def handleFavoritNode(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
if aor:
|
||||
interface.getNode(myNodeNumber).setFavorite(nodeID)
|
||||
logger.info(f"System: Added {nodeID} to favorites for device {nodeInt}")
|
||||
else:
|
||||
interface.getNode(myNodeNumber).removeFavorite(nodeID)
|
||||
logger.info(f"System: Removed {nodeID} from favorites for device {nodeInt}")
|
||||
|
||||
def getFavoritNodes(nodeInt=1):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
favList = []
|
||||
for node in interface.getNode(myNodeNumber).favorites:
|
||||
favList.append(node)
|
||||
return favList
|
||||
|
||||
def handleSentinelIgnore(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
if aor:
|
||||
sentryIgnoreList.append(str(nodeID))
|
||||
logger.info(f"System: Added {nodeID} to sentry ignore list")
|
||||
else:
|
||||
sentryIgnoreList.remove(str(nodeID))
|
||||
logger.info(f"System: Removed {nodeID} from sentry ignore list")
|
||||
|
||||
|
||||
def messageChunker(message):
|
||||
message_list = []
|
||||
if len(message) > MESSAGE_CHUNK_SIZE:
|
||||
@@ -444,9 +612,9 @@ def messageChunker(message):
|
||||
sentence = ''
|
||||
for char in part:
|
||||
sentence += char
|
||||
if char in '.!?':
|
||||
sentences.append(sentence.strip())
|
||||
sentence = ''
|
||||
# if char in '.!?':
|
||||
# sentences.append(sentence.strip())
|
||||
# sentence = ''
|
||||
if sentence:
|
||||
sentences.append(sentence.strip())
|
||||
|
||||
@@ -476,12 +644,25 @@ def messageChunker(message):
|
||||
if current_chunk:
|
||||
message_list.append(current_chunk)
|
||||
|
||||
# Consolidate any adjacent messages that can fit in a single chunk.
|
||||
idx = 0
|
||||
while idx < len(message_list) - 1:
|
||||
if len(message_list[idx]) + len(message_list[idx+1]) < MESSAGE_CHUNK_SIZE:
|
||||
message_list[idx] += '\n' + message_list[idx+1]
|
||||
del message_list[idx+1]
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
# Ensure no chunk exceeds MESSAGE_CHUNK_SIZE
|
||||
final_message_list = []
|
||||
for chunk in message_list:
|
||||
while len(chunk) > MESSAGE_CHUNK_SIZE:
|
||||
final_message_list.append(chunk[:MESSAGE_CHUNK_SIZE])
|
||||
chunk = chunk[MESSAGE_CHUNK_SIZE:]
|
||||
# Find the last space within the chunk size limit
|
||||
split_index = chunk.rfind(' ', 0, MESSAGE_CHUNK_SIZE)
|
||||
if split_index == -1:
|
||||
split_index = MESSAGE_CHUNK_SIZE
|
||||
final_message_list.append(chunk[:split_index])
|
||||
chunk = chunk[split_index:].strip()
|
||||
if chunk:
|
||||
final_message_list.append(chunk)
|
||||
|
||||
@@ -593,11 +774,24 @@ def messageTrap(msg):
|
||||
# Split Message on assumed words spaces m for m = msg.split(" ")
|
||||
# t in trap_list, built by the config and system.py not the user
|
||||
message_list=msg.split(" ")
|
||||
|
||||
if cmdBang:
|
||||
# check for ! at the start of the message to force a command
|
||||
if not message_list[0].startswith('!'):
|
||||
return False
|
||||
else:
|
||||
message_list[0] = message_list[0][1:]
|
||||
|
||||
for m in message_list:
|
||||
for t in trap_list:
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
return True
|
||||
if not explicitCmd:
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
return True
|
||||
else:
|
||||
# if the index 0 of the message is a word in the trap list, return True
|
||||
if t.lower() == m.lower() and message_list.index(m) == 0:
|
||||
return True
|
||||
# if no trap words found, run a search for near misses like ping? or cmd?
|
||||
for m in message_list:
|
||||
for t in range(len(trap_list)):
|
||||
@@ -653,11 +847,17 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
multiPingList.pop(j)
|
||||
break
|
||||
|
||||
|
||||
priorVolcanoAlert = ""
|
||||
priorEmergencyAlert = ""
|
||||
priorWxAlert = ""
|
||||
def handleAlertBroadcast(deviceID=1):
|
||||
global priorVolcanoAlert, priorEmergencyAlert, priorWxAlert
|
||||
alertUk = NO_ALERTS
|
||||
alertDe = NO_ALERTS
|
||||
alertFema = NO_ALERTS
|
||||
wxAlert = NO_ALERTS
|
||||
volcanoAlert = NO_ALERTS
|
||||
alertWx = False
|
||||
# only allow API call every 20 minutes
|
||||
# the watchdog will call this function 3 times, seeing possible throttling on the API
|
||||
clock = datetime.now()
|
||||
@@ -671,6 +871,8 @@ def handleAlertBroadcast(deviceID=1):
|
||||
alertWx = alertBrodcastNOAA()
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if enableDEalerts:
|
||||
alertDe = get_nina_alerts()
|
||||
if enableGBalerts:
|
||||
alertUk = get_govUK_alerts()
|
||||
else:
|
||||
@@ -679,15 +881,20 @@ def handleAlertBroadcast(deviceID=1):
|
||||
|
||||
# format alert
|
||||
if alertWx:
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS-WX ALERT: {alertWx[0]}"
|
||||
else:
|
||||
wxAlert = False
|
||||
|
||||
femaAlert = alertFema
|
||||
ukAlert = alertUk
|
||||
deAlert = alertDe
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert:
|
||||
if femaAlert != priorEmergencyAlert:
|
||||
priorEmergencyAlert = femaAlert
|
||||
else:
|
||||
return False
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(femaAlert, int(channel), 0, deviceID)
|
||||
@@ -695,60 +902,65 @@ def handleAlertBroadcast(deviceID=1):
|
||||
send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
if NO_ALERTS not in ukAlert:
|
||||
if ukAlert != priorEmergencyAlert:
|
||||
priorEmergencyAlert = ukAlert
|
||||
else:
|
||||
return False
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(ukAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
if NO_ALERTS not in alertDe:
|
||||
if deAlert != priorEmergencyAlert:
|
||||
priorEmergencyAlert = deAlert
|
||||
else:
|
||||
return False
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(deAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for 10 seconds
|
||||
time.sleep(10)
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
if wxAlert:
|
||||
if wxAlert != priorWxAlert:
|
||||
priorWxAlert = wxAlert
|
||||
else:
|
||||
return False
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(wxAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue)
|
||||
if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert:
|
||||
# check if the alert is different from the last one
|
||||
if volcanoAlert != priorVolcanoAlert:
|
||||
priorVolcanoAlert = volcanoAlert
|
||||
if isinstance(volcanoAlertBroadcastChannel, list):
|
||||
for channel in volcanoAlertBroadcastChannel:
|
||||
send_message(volcanoAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(volcanoAlert, volcanoAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
rxType = type(interface).__name__
|
||||
if rxType in ['SerialInterface', 'TCPInterface', 'BLEInterface']:
|
||||
identifier = interface.__dict__.get('devPath', interface.__dict__.get('hostname', 'BLE'))
|
||||
logger.critical(f"System: Lost Connection to Device {identifier}")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
if (rxType == 'SerialInterface' and globals().get(f'port{i}') in identifier) or \
|
||||
(rxType == 'TCPInterface' and globals().get(f'hostname{i}') in identifier) or \
|
||||
(rxType == 'BLEInterface' and globals().get(f'interface{i}_type') == 'ble'):
|
||||
globals()[f'retry_int{i}'] = True
|
||||
break
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
logger.debug(f"System: Closing Interface1")
|
||||
interface1.close()
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
logger.debug(f"System: Closing Interface{i}")
|
||||
globals()[f'interface{i}'].close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
save_bbsdb()
|
||||
save_bbsdm()
|
||||
logger.debug(f"System: BBS Messages Saved")
|
||||
logger.debug(f"System: Exiting")
|
||||
asyncLoop.stop()
|
||||
asyncLoop.close()
|
||||
exit (0)
|
||||
# Handle disconnection of the interface
|
||||
logger.warning(f"System: Abrupt Disconnection of Interface detected")
|
||||
interface.close()
|
||||
|
||||
# Telemetry Functions
|
||||
telemetryData = {}
|
||||
@@ -775,6 +987,23 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
return fwVer
|
||||
return -1
|
||||
|
||||
def compileFavoriteList():
|
||||
# build a list of favorite nodes to add to the device
|
||||
fav_list = []
|
||||
if (bbs_admin_list != [0] or favoriteNodeList != ['']) or bbs_link_whitelist != [0]:
|
||||
logger.debug(f"System: Collecting Favorite Nodes to add to device(s)")
|
||||
# loop through each interface and add the favorite nodes
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}') and globals().get(f'interface{i}_enabled'):
|
||||
for fav in bbs_admin_list + favoriteNodeList + bbs_link_whitelist:
|
||||
if fav != 0 and fav != '' and fav is not None:
|
||||
object = {'nodeID': fav, 'deviceID': i}
|
||||
# check object not already in the list
|
||||
if object not in fav_list:
|
||||
fav_list.append(object)
|
||||
logger.debug(f"System: Adding Favorite Node {fav} to Device {i}")
|
||||
return fav_list
|
||||
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
interface = globals()[f'interface{rxNode}']
|
||||
myNodeNum = globals().get(f'myNodeNum{rxNode}')
|
||||
@@ -832,26 +1061,39 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
|
||||
if batteryLevel < 25:
|
||||
logger.warning(f"System: Low Battery Level: {batteryLevel}{emji} on Device: {rxNode}")
|
||||
send_message(f"Low Battery Level: {batteryLevel}{emji} on Device: {rxNode}", {secure_channel}, 0, {secure_interface})
|
||||
elif batteryLevel < 10:
|
||||
logger.critical(f"System: Critical Battery Level: {batteryLevel}{emji} on Device: {rxNode}")
|
||||
return dataResponse
|
||||
|
||||
positionMetadata = {}
|
||||
def consumeMetadata(packet, rxNode=0):
|
||||
def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
global positionMetadata, telemetryData
|
||||
|
||||
# check type of packet
|
||||
try:
|
||||
# keep records of recent telemetry data
|
||||
packet_type = ''
|
||||
if packet.get('decoded'):
|
||||
packet_type = packet['decoded']['portnum']
|
||||
nodeID = packet['from']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Metadata decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# TELEMETRY packets
|
||||
if packet_type == 'TELEMETRY_APP':
|
||||
if debugMetadata: print(f"DEBUG TELEMETRY_APP: {packet}\n\n")
|
||||
# get the telemetry data
|
||||
# TELEMETRY packets
|
||||
if packet_type == 'TELEMETRY_APP':
|
||||
if debugMetadata and 'TELEMETRY_APP' not in metadataFilter:
|
||||
print(f"DEBUG TELEMETRY_APP: {packet}\n\n")
|
||||
# get the telemetry data
|
||||
try:
|
||||
telemetry_packet = packet['decoded']['telemetry']
|
||||
if telemetry_packet.get('deviceMetrics'):
|
||||
deviceMetrics = telemetry_packet['deviceMetrics']
|
||||
#if uptime is in deviceMetrics and uptime is not 0 set uptime
|
||||
# if deviceMetrics.get('uptimeSeconds') is not None and deviceMetrics['uptimeSeconds'] != 0:
|
||||
# if highestUptime < deviceMetrics['uptimeSeconds']:
|
||||
# highestUptime = deviceMetrics['uptimeSeconds']
|
||||
# highestUptimeNode = nodeID
|
||||
|
||||
if telemetry_packet.get('localStats'):
|
||||
localStats = telemetry_packet['localStats']
|
||||
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
|
||||
@@ -864,66 +1106,164 @@ def consumeMetadata(packet, rxNode=0):
|
||||
for key in keys:
|
||||
if localStats.get(key) is not None:
|
||||
telemetryData[rxNode][key] = localStats.get(key)
|
||||
|
||||
# POSITION_APP packets
|
||||
if packet_type == 'POSITION_APP':
|
||||
if debugMetadata: print(f"DEBUG POSITION_APP: {packet}\n\n")
|
||||
# get the position data
|
||||
keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_data = packet['decoded']['position']
|
||||
try:
|
||||
if nodeID not in positionMetadata:
|
||||
positionMetadata[nodeID] = {}
|
||||
|
||||
for key in keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# Keep the positionMetadata dictionary at 5 records
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
del positionMetadata[oldest_nodeID]
|
||||
except Exception as e:
|
||||
logger.debug(f"System: POSITION_APP decode error: {e} packet {packet}")
|
||||
except Exception as e:
|
||||
logger.debug(f"System: TELEMETRY_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# WAYPOINT_APP packets
|
||||
if packet_type == 'WAYPOINT_APP':
|
||||
if debugMetadata: print(f"DEBUG WAYPOINT_APP: {packet['decoded']['waypoint']}\n\n")
|
||||
# get the waypoint data
|
||||
waypoint_data = packet['decoded']
|
||||
# POSITION_APP packets
|
||||
if packet_type == 'POSITION_APP':
|
||||
if debugMetadata and 'POSITION_APP' not in metadataFilter:
|
||||
print(f"DEBUG POSITION_APP: {packet}\n\n")
|
||||
# get the position data
|
||||
keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_data = packet['decoded']['position']
|
||||
try:
|
||||
if nodeID not in positionMetadata:
|
||||
positionMetadata[nodeID] = {}
|
||||
|
||||
for key in keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# NEIGHBORINFO_APP
|
||||
if packet_type == 'NEIGHBORINFO_APP':
|
||||
if debugMetadata: print(f"DEBUG NEIGHBORINFO_APP: {packet}\n\n")
|
||||
# get the neighbor info data
|
||||
neighbor_data = packet['decoded']
|
||||
|
||||
# TRACEROUTE_APP
|
||||
if packet_type == 'TRACEROUTE_APP':
|
||||
if debugMetadata: print(f"DEBUG TRACEROUTE_APP: {packet}\n\n")
|
||||
# get the traceroute data
|
||||
traceroute_data = packet['decoded']
|
||||
# if altitude is over highfly_altitude send a log and message for high-flying nodes and not in highfly_ignoreList
|
||||
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList:
|
||||
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} Channel: {channel} NodeID:{nodeID} Lat:{position_data.get('latitude', 0)} Lon:{position_data.get('longitude', 0)}")
|
||||
altFeet = round(position_data['altitude'] * 3.28084, 2)
|
||||
msg = f"🚀 High Altitude Detected! NodeID:{nodeID} Alt:{altFeet:,.0f}ft/{position_data['altitude']:,.0f}m"
|
||||
|
||||
# DETECTION_SENSOR_APP
|
||||
if packet_type == 'DETECTION_SENSOR_APP':
|
||||
if debugMetadata: print(f"DEBUG DETECTION_SENSOR_APP: {packet}\n\n")
|
||||
# get the detection sensor data
|
||||
detection_data = packet['decoded']
|
||||
if highfly_check_openskynetwork:
|
||||
# check get_openskynetwork to see if the node is an aircraft
|
||||
if 'latitude' in position_data and 'longitude' in position_data:
|
||||
flight_info = get_openskynetwork(position_data.get('latitude', 0), position_data.get('longitude', 0))
|
||||
if flight_info and NO_ALERTS not in flight_info and ERROR_FETCHING_DATA not in flight_info:
|
||||
msg += f"\n✈️Detected near:\n{flight_info}"
|
||||
|
||||
# PAXCOUNTER_APP
|
||||
if packet_type == 'PAXCOUNTER_APP':
|
||||
if debugMetadata: print(f"DEBUG PAXCOUNTER_APP: {packet}\n\n")
|
||||
# get the paxcounter data
|
||||
paxcounter_data = packet['decoded']
|
||||
send_message(msg, highfly_channel, 0, highfly_interface)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# REMOTE_HARDWARE_APP
|
||||
if packet_type == 'REMOTE_HARDWARE_APP':
|
||||
if debugMetadata: print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
|
||||
# get the remote hardware data
|
||||
remote_hardware_data = packet['decoded']
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error consuming metadata: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
# Keep the positionMetadata dictionary at a maximum size of 20
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
del positionMetadata[oldest_nodeID]
|
||||
|
||||
# add a packet count to the positionMetadata for the node
|
||||
if 'packetCount' in positionMetadata[nodeID]:
|
||||
positionMetadata[nodeID]['packetCount'] += 1
|
||||
else:
|
||||
positionMetadata[nodeID]['packetCount'] = 1
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: POSITION_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# WAYPOINT_APP packets
|
||||
if packet_type == 'WAYPOINT_APP':
|
||||
if debugMetadata and 'WAYPOINT_APP' not in metadataFilter:
|
||||
print(f"DEBUG WAYPOINT_APP: {packet}\n\n")
|
||||
# get the waypoint data
|
||||
waypoint_data = packet['decoded']['waypoint']
|
||||
try:
|
||||
id = waypoint_data.get('id', 0)
|
||||
latitudeI = waypoint_data.get('latitudeI', 0)
|
||||
longitudeI = waypoint_data.get('longitudeI', 0)
|
||||
expire = waypoint_data.get('expire', 0)
|
||||
if expire == 1:
|
||||
expire = "Now"
|
||||
elif expire == 0:
|
||||
expire = "Never"
|
||||
else:
|
||||
expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expire))
|
||||
description = waypoint_data.get('description', '')
|
||||
name = waypoint_data.get('name', '')
|
||||
logger.info(f"System: Waypoint from Device: {rxNode} Channel: {channel} NodeID:{nodeID} ID:{id} Lat:{latitudeI/1e7} Lon:{longitudeI/1e7} Expire:{expire} Name:{name} Desc:{description}")
|
||||
except Exception as e:
|
||||
logger.debug(f"System: WAYPOINT_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# NEIGHBORINFO_APP
|
||||
if packet_type == 'NEIGHBORINFO_APP':
|
||||
if debugMetadata and 'NEIGHBORINFO_APP' not in metadataFilter:
|
||||
print(f"DEBUG NEIGHBORINFO_APP: {packet}\n\n")
|
||||
# get the neighbor info data
|
||||
neighbor_data = packet['decoded']
|
||||
neighbor_list = neighbor_data.get('neighbors', [])
|
||||
logger.info(f"System: Neighbor Info from Device: {rxNode} Channel: {channel} NodeID:{nodeID} Neighbors:{len(neighbor_list)}")
|
||||
|
||||
# TRACEROUTE_APP
|
||||
if packet_type == 'TRACEROUTE_APP':
|
||||
if debugMetadata and 'TRACEROUTE_APP' not in metadataFilter:
|
||||
print(f"DEBUG TRACEROUTE_APP: {packet}\n\n")
|
||||
# get the traceroute data
|
||||
traceroute_data = packet['decoded']
|
||||
|
||||
# DETECTION_SENSOR_APP
|
||||
if packet_type == 'DETECTION_SENSOR_APP':
|
||||
if debugMetadata and 'DETECTION_SENSOR_APP' not in metadataFilter:
|
||||
print(f"DEBUG DETECTION_SENSOR_APP: {packet}\n\n")
|
||||
# get the detection sensor data
|
||||
detection_data = packet['decoded']
|
||||
detction_text = detection_data.get('text', '')
|
||||
try:
|
||||
if detction_text != '':
|
||||
logger.info(f"System: Detection Sensor Data from Device: {rxNode} Channel: {channel} NodeID:{nodeID} Text:{detction_text}")
|
||||
if detctionSensorAlert:
|
||||
send_message(f"🚨Detection Sensor from Device: {rxNode} Channel: {channel} NodeID:{get_name_from_number(nodeID,'long',rxNode)} Alert:{detction_text}", secure_channel, 0, secure_interface)
|
||||
time.sleep(responseDelay)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: DETECTION_SENSOR_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# PAXCOUNTER_APP
|
||||
if packet_type == 'PAXCOUNTER_APP':
|
||||
if debugMetadata and 'PAXCOUNTER_APP' not in metadataFilter:
|
||||
print(f"DEBUG PAXCOUNTER_APP: {packet}\n\n")
|
||||
# get the paxcounter data
|
||||
paxcounter_data = packet['decoded']['paxcounter']
|
||||
try:
|
||||
wifi_count = paxcounter_data.get('wifi', 0)
|
||||
ble_count = paxcounter_data.get('ble', 0)
|
||||
uptime = paxcounter_data.get('uptime', 0)
|
||||
logger.info(f"System: Paxcounter Data from Device: {rxNode} Channel: {channel} NodeID:{nodeID} WiFi:{wifi_count} BLE:{ble_count} Uptime:{getPrettyTime(uptime)}")
|
||||
except Exception as e:
|
||||
logger.debug(f"System: PAXCOUNTER_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# REMOTE_HARDWARE_APP
|
||||
if packet_type == 'REMOTE_HARDWARE_APP':
|
||||
if debugMetadata and 'REMOTE_HARDWARE_APP' not in metadataFilter:
|
||||
print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
|
||||
# get the remote hardware data
|
||||
remote_hardware_data = packet['decoded']
|
||||
try:
|
||||
hardware_info = remote_hardware_data.get('hardware_info', '')
|
||||
logger.info(f"System: Remote Hardware Data from Device: {rxNode} Channel: {channel} NodeID:{nodeID} Info:{hardware_info}")
|
||||
except Exception as e:
|
||||
logger.debug(f"System: REMOTE_HARDWARE_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# ADMIN_APP
|
||||
|
||||
# IP_TUNNEL_APP
|
||||
|
||||
# SERIAL_APP
|
||||
|
||||
# STORE_FOWARD_APP
|
||||
|
||||
# RANGE_TEST_APP
|
||||
|
||||
# COMPRESSED_TEXT_APP
|
||||
|
||||
# AUDIO_APP
|
||||
|
||||
# SIMULATOR_APP
|
||||
return True
|
||||
|
||||
def noisyTelemetryCheck():
|
||||
global positionMetadata
|
||||
if len(positionMetadata) == 0:
|
||||
return
|
||||
# sort the positionMetadata by packetCount
|
||||
sorted_positionMetadata = dict(sorted(positionMetadata.items(), key=lambda item: item[1].get('packetCount', 0), reverse=True))
|
||||
top_three = list(sorted_positionMetadata.items())[:3]
|
||||
for nodeID, data in top_three:
|
||||
if data.get('packetCount', 0) > noisyTelemetryLimit:
|
||||
logger.warning(f"System: Noisy Telemetry Detected from NodeID:{nodeID} ShortName:{get_name_from_number(nodeID, 'short', 1)} Packets:{data.get('packetCount', 0)}")
|
||||
# reset the packet count for the node
|
||||
positionMetadata[nodeID]['packetCount'] = 0
|
||||
|
||||
def get_sysinfo(nodeID=0, deviceID=1):
|
||||
# Get the system telemetry data for return on the sysinfo command
|
||||
@@ -995,7 +1335,7 @@ async def handleFileWatcher():
|
||||
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
|
||||
if type(file_monitor_broadcastCh) is list:
|
||||
for ch in file_monitor_broadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
if antiSpam and int(ch) != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
@@ -1021,21 +1361,26 @@ async def handleFileWatcher():
|
||||
pass
|
||||
|
||||
async def retry_interface(nodeID):
|
||||
global max_retry_count
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
global max_retry_count1, max_retry_count2, max_retry_count3, max_retry_count4, max_retry_count5, max_retry_count6, max_retry_count7, max_retry_count8, max_retry_count9
|
||||
interface = globals()[f'interface{nodeID}']
|
||||
retry_int = globals()[f'retry_int{nodeID}']
|
||||
max_retry_count = globals()[f'max_retry_count{nodeID}']
|
||||
|
||||
if dont_retry_disconnect:
|
||||
logger.critical(f"System: dont_retry_disconnect is set, not retrying interface{nodeID}")
|
||||
exit_handler()
|
||||
|
||||
if interface is not None:
|
||||
retry_int = True
|
||||
max_retry_count -= 1
|
||||
globals()[f'retry_int{nodeID}'] = True
|
||||
globals()[f'max_retry_count{nodeID}'] -= 1
|
||||
logger.debug(f"System: Retrying interface{nodeID} {globals()[f'max_retry_count{nodeID}']} attempts left")
|
||||
try:
|
||||
interface.close()
|
||||
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing interface{nodeID}: {e}")
|
||||
|
||||
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
|
||||
if max_retry_count == 0:
|
||||
if globals()[f'max_retry_count{nodeID}'] == 0:
|
||||
logger.critical(f"System: Max retry count reached for interface{nodeID}")
|
||||
exit_handler()
|
||||
|
||||
@@ -1045,15 +1390,19 @@ async def retry_interface(nodeID):
|
||||
if retry_int:
|
||||
interface = None
|
||||
globals()[f'interface{nodeID}'] = None
|
||||
logger.debug(f"System: Retrying Interface{nodeID}")
|
||||
interface_type = globals()[f'interface{nodeID}_type']
|
||||
if interface_type == 'serial':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} Serial on port: {globals().get(f'port{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}'))
|
||||
elif interface_type == 'tcp':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} TCP on hostname: {globals().get(f'hostname{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}'))
|
||||
elif interface_type == 'ble':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} BLE on mac: {globals().get(f'mac{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}'))
|
||||
logger.debug(f"System: Interface{nodeID} Opened!")
|
||||
# reset the retry_int and retry_count
|
||||
globals()[f'max_retry_count{nodeID}'] = interface_retry_count
|
||||
globals()[f'retry_int{nodeID}'] = False
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
|
||||
@@ -1093,10 +1442,8 @@ async def handleSentinel(deviceID):
|
||||
resolution = metadata.get('precisionBits')
|
||||
|
||||
logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, i)
|
||||
time.sleep(responseDelay + 1)
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
|
||||
time.sleep(responseDelay + 1)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
@@ -1127,7 +1474,7 @@ async def watchdog():
|
||||
|
||||
handleMultiPing(0, i)
|
||||
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
|
||||
handleAlertBroadcast(i)
|
||||
|
||||
intData = displayNodeTelemetry(0, i)
|
||||
@@ -1140,4 +1487,38 @@ async def watchdog():
|
||||
await retry_interface(i)
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface{i}: {e}")
|
||||
|
||||
# check for noisy telemetry
|
||||
if noisyNodeLogging:
|
||||
noisyTelemetryCheck()
|
||||
|
||||
# check the load_bbsdm flag to reload the BBS messages from disk
|
||||
if bbs_enabled and bbsAPI_enabled:
|
||||
load_bbsdm()
|
||||
load_bbsdb()
|
||||
|
||||
# perform memory cleanup every 10 minutes
|
||||
if datetime.now().minute % 10 == 0:
|
||||
cleanup_memory()
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
logger.debug(f"System: Closing Interface1")
|
||||
interface1.close()
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
logger.debug(f"System: Closing Interface{i}")
|
||||
globals()[f'interface{i}'].close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
save_bbsdb()
|
||||
save_bbsdm()
|
||||
logger.debug(f"System: BBS Messages Saved")
|
||||
logger.debug(f"System: Exiting")
|
||||
asyncLoop.stop()
|
||||
asyncLoop.close()
|
||||
exit (0)
|
||||
|
||||
@@ -7,17 +7,26 @@
|
||||
import os
|
||||
import http.server
|
||||
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
|
||||
# Set the port for the server
|
||||
PORT = 8420
|
||||
|
||||
# set webRoot index.html location
|
||||
webRoot = "etc/www"
|
||||
# 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
|
||||
|
||||
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
|
||||
SSL = 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
|
||||
@@ -31,8 +40,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
# Change the current working directory to webRoot
|
||||
os.chdir(webRoot)
|
||||
|
||||
# boot up simple HTTP server
|
||||
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
|
||||
# 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)
|
||||
@@ -42,8 +51,10 @@ if SSL:
|
||||
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")
|
||||
|
||||
print(f"Serving reports at http://localhost:{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
|
||||
|
||||
154
pong_bot.py
154
pong_bot.py
@@ -25,11 +25,11 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
command_handler = {
|
||||
# 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: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"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),
|
||||
"echo": lambda: handle_echo(message, message_from_id, deviceID, isDM, channel_number),
|
||||
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
@@ -56,6 +56,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
|
||||
return bot_response
|
||||
|
||||
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:
|
||||
@@ -140,14 +147,48 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
|
||||
return msg
|
||||
|
||||
def handle_motd(message):
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
if "$" in message:
|
||||
isAdmin = False
|
||||
msg = ""
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
if str(message_from_id) == admin:
|
||||
isAdmin = True
|
||||
break
|
||||
else:
|
||||
isAdmin = True
|
||||
|
||||
# admin help via DM
|
||||
if "?" in message and isDM and isAdmin:
|
||||
msg = "Message of the day, set with 'motd $ HelloWorld!'"
|
||||
elif "?" in message and isDM and not isAdmin:
|
||||
# non-admin help via DM
|
||||
msg = "Message of the day"
|
||||
elif "$" in message and isAdmin:
|
||||
motd = message.split("$")[1]
|
||||
MOTD = motd.rstrip()
|
||||
return "MOTD Set to: " + MOTD
|
||||
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
|
||||
msg = "MOTD changed to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
msg = "MOTD: " + MOTD
|
||||
return msg
|
||||
|
||||
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
|
||||
if "?" in message.lower():
|
||||
return "echo command returns your message back to you. Example:echo Hello World"
|
||||
elif "echo " in message.lower():
|
||||
parts = message.lower().split("echo ", 1)
|
||||
if len(parts) > 1 and parts[1].strip() != "":
|
||||
echo_msg = parts[1]
|
||||
if channel_number != echoChannel:
|
||||
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
|
||||
return echo_msg
|
||||
else:
|
||||
return "Please provide a message to echo back to you. Example:echo Hello World"
|
||||
else:
|
||||
return "Please provide a message to echo back to you. Example:echo Hello World"
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
@@ -163,14 +204,6 @@ def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
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
|
||||
|
||||
@@ -211,15 +244,15 @@ def onReceive(packet, interface):
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
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 rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
@@ -248,6 +281,7 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
@@ -277,10 +311,17 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
@@ -304,7 +345,7 @@ def onReceive(packet, interface):
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
# log the message to stdout
|
||||
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
|
||||
@@ -315,7 +356,8 @@ def onReceive(packet, interface):
|
||||
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-'))
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
@@ -374,7 +416,7 @@ def onReceive(packet, interface):
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode, channel_number)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
@@ -398,10 +440,12 @@ async def start_rx():
|
||||
logger.debug("System: Celestial Telemetry Enabled")
|
||||
if motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if enableEcho:
|
||||
logger.debug(f"System: Echo command Enabled")
|
||||
if 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}")
|
||||
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and multiple_interface:
|
||||
@@ -426,21 +470,49 @@ async def start_rx():
|
||||
|
||||
# Hello World
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
if file_monitor_enabled:
|
||||
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
if file_monitor_enabled:
|
||||
await asyncio.gather(fileMonTask)
|
||||
tasks = []
|
||||
|
||||
try:
|
||||
# Create core tasks
|
||||
tasks.append(asyncio.create_task(start_rx(), name="pong_rx"))
|
||||
tasks.append(asyncio.create_task(watchdog(), name="watchdog"))
|
||||
|
||||
# Add optional tasks
|
||||
if file_monitor_enabled:
|
||||
tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor"))
|
||||
|
||||
logger.debug(f"System: Starting {len(tasks)} async tasks")
|
||||
|
||||
# Wait for all tasks with proper exception handling
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Check for exceptions in results
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Task {tasks[i].get_name()} failed with: {result}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Main loop error: {e}")
|
||||
finally:
|
||||
# Cleanup tasks
|
||||
logger.debug("System: Cleaning up async tasks")
|
||||
for task in tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"Task {task.get_name()} cancelled successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cancelling task {task.get_name()}: {e}")
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
except SystemExit:
|
||||
pass
|
||||
# EOF
|
||||
|
||||
43
script/addFav.py
Normal file
43
script/addFav.py
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# Add a favorite node to all interfaces from config.ini data
|
||||
# meshing-around - helper script
|
||||
import sys
|
||||
import os
|
||||
|
||||
# welcome header
|
||||
print("meshing-around: addFav - Auto-Add favorite nodes to all interfaces from config.ini data")
|
||||
print("---------------------------------------------------------------")
|
||||
|
||||
try:
|
||||
# set the path to import the modules and config.ini
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
except Exception as e:
|
||||
print(f"Error importing modules run this program from the main program directory 'python3 script/addFav.py'")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
# compile the favorite list wich returns node,interface tuples
|
||||
favList = compileFavoriteList()
|
||||
logger.debug(f"addFav: Compiled favorite list:\n {favList}")
|
||||
except Exception as e:
|
||||
logger.error(f"addFav: Error compiling favorite list: {e} - run this program from the main program directory 'python3 script/addFav.py'")
|
||||
exit(1)
|
||||
|
||||
if favList:
|
||||
# for each node,interface tuple add the favorite node
|
||||
for fav in favList:
|
||||
try:
|
||||
handleFavoritNode(fav['deviceID'], fav['nodeID'], True)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"addFav: Error adding favorite node {fav['nodeID']} to device {fav['deviceID']}: {e}")
|
||||
else:
|
||||
logger.info("addFav: No favorite nodes to add to device(s)")
|
||||
exit(0)
|
||||
|
||||
count_devices = set([fav['deviceID'] for fav in favList])
|
||||
count_nodes = set([fav['nodeID'] for fav in favList])
|
||||
logger.info(f"addFav: Finished adding {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)")
|
||||
exit(0)
|
||||
@@ -4,6 +4,8 @@ This is not a full turnkey setup for Docker yet but gets you most of the way the
|
||||
## 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`
|
||||
@@ -12,6 +14,9 @@ still a WIP
|
||||
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:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
REM launch meshing-around container with a terminal
|
||||
docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"
|
||||
docker run -it --entrypoint /bin/bash meshing-around
|
||||
53
script/injectDM.py
Normal file
53
script/injectDM.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
# Usage: python3 script/injectDM.py -s NODEID -d NODEID -m "message"
|
||||
# meshing-around - helper script
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
# welcome header
|
||||
print("meshing-around: injectDM.py -s NODEID -d NODEID -m 'Hello World'")
|
||||
print("Auto-Inject DM messages to data/bbsdm.pkl")
|
||||
print(" needs config.ini [bbs] bbsAPI_enabled = True ")
|
||||
print("---------------------------------------------------------------")
|
||||
|
||||
try:
|
||||
# set the path to import the modules and config.ini
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from modules.log import *
|
||||
from modules.bbstools import *
|
||||
except Exception as e:
|
||||
print(f"Error importing modules run this program from the main program directory 'python3 script/injectDM.py'")
|
||||
exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Inject DM messages to data/bbsdm.pkl')
|
||||
parser.add_argument('-s', '--src', type=str, required=True, help='Source NODEID')
|
||||
parser.add_argument('-d', '--dst', type=str, required=True, help='Destination NODEID')
|
||||
parser.add_argument('-m', '--msg', type=str, required=True, help="'Message to send'")
|
||||
args = parser.parse_args()
|
||||
dst = args.dst
|
||||
src = args.src
|
||||
message = args.msg
|
||||
if not message:
|
||||
logger.error("Message cannot be empty")
|
||||
exit(1)
|
||||
if dst == src:
|
||||
logger.error("Source and Destination cannot be the same")
|
||||
exit(1)
|
||||
|
||||
if not isinstance(bbs_dm, list):
|
||||
logger.error("bbs_dm is corrupt, something is wrong")
|
||||
exit(1)
|
||||
|
||||
# inject the message
|
||||
if bbs_post_dm(dst, message, src):
|
||||
logger.info(f"Injected message from {src} to {dst}: {message}")
|
||||
else:
|
||||
logger.error("Failed to inject message")
|
||||
exit(1)
|
||||
|
||||
# show stats get_bbs_stats
|
||||
stats = get_bbs_stats()
|
||||
stats = stats.replace("\n", " | ")
|
||||
logger.info(f"BBS Stats: {stats}")
|
||||
@@ -24,4 +24,21 @@ else
|
||||
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"
|
||||
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
|
||||
|
||||
# attempt check for updates
|
||||
if command -v git &> /dev/null
|
||||
then
|
||||
if [ -d ../.git ]; then
|
||||
# check for updates
|
||||
git fetch --quiet
|
||||
local_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$local_branch" != "HEAD" ] && git show-ref --verify --quiet "refs/remotes/origin/$local_branch"; then
|
||||
local_commit=$(git rev-parse "$local_branch")
|
||||
remote_commit=$(git rev-parse "origin/$local_branch")
|
||||
if [ "$local_commit" != "$remote_commit" ]; then
|
||||
echo "Bot Update Available!"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
71
update.sh
Normal file
71
update.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
# MeshBot Update Script
|
||||
# Usage: bash update.sh or ./update.sh after making it executable with chmod +x update.sh
|
||||
|
||||
# Check if the mesh_bot.service or pong_bot.service
|
||||
if systemctl is-active --quiet mesh_bot.service; then
|
||||
echo "Stopping mesh_bot.service..."
|
||||
systemctl stop mesh_bot.service
|
||||
service_stopped=true
|
||||
fi
|
||||
if systemctl is-active --quiet pong_bot.service; then
|
||||
echo "Stopping pong_bot.service..."
|
||||
systemctl stop pong_bot.service
|
||||
service_stopped=true
|
||||
fi
|
||||
if systemctl is-active --quiet mesh_bot_reporting.service; then
|
||||
echo "Stopping mesh_bot_reporting.service..."
|
||||
systemctl stop mesh_bot_reporting.service
|
||||
service_stopped=true
|
||||
fi
|
||||
if systemctl is-active --quiet mesh_bot_w3.service; then
|
||||
echo "Stopping mesh_bot_w3.service..."
|
||||
systemctl stop mesh_bot_w3.service
|
||||
service_stopped=true
|
||||
fi
|
||||
|
||||
# git pull with rebase to avoid unnecessary merge commits
|
||||
echo "Pulling latest changes from GitHub..."
|
||||
if ! git pull origin main --rebase; then
|
||||
read -p "Git pull resulted in conflicts. Do you want to reset hard to origin/main? This will discard local changes. (y/n): " choice
|
||||
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
echo "Local repository updated."
|
||||
else
|
||||
echo "Update aborted due to git conflicts."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install or update dependencies
|
||||
echo "Installing or updating dependencies..."
|
||||
if pip install -r requirements.txt --upgrade 2>&1 | grep -q "externally-managed-environment"; then
|
||||
# if venv is found ask to run with launch.sh
|
||||
if [ -d "venv" ]; then
|
||||
echo "A virtual environment (venv) was found. run from inside venv"
|
||||
else
|
||||
read -p "Warning: You are in an externally managed environment. Do you want to continue with --break-system-packages? (y/n): " choice
|
||||
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
|
||||
pip install --break-system-packages -r requirements.txt --upgrade
|
||||
else
|
||||
echo "Update aborted due to dependency installation issue."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Dependencies installed or updated."
|
||||
fi
|
||||
|
||||
# if service was stopped earlier, restart it
|
||||
if [ "$service_stopped" = true ]; then
|
||||
echo "Restarting services..."
|
||||
systemctl start mesh_bot.service
|
||||
systemctl start pong_bot.service
|
||||
systemctl start mesh_bot_reporting.service
|
||||
systemctl start mesh_bot_w3.service
|
||||
echo "Services restarted."
|
||||
fi
|
||||
|
||||
# Print completion message
|
||||
echo "Update completed successfully?"
|
||||
exit 0
|
||||
# End of script
|
||||
Reference in New Issue
Block a user