Compare commits

...

424 Commits

Author SHA1 Message Date
Kelly
8dcbf66618 Merge pull request #108 from SpudGunMan/lab
Enhancement from Labwork
2025-01-12 13:09:14 -08:00
SpudGunMan
902b4f22ee readme 2025-01-12 12:43:44 -08:00
SpudGunMan
7ae0d5e927 Update pong_bot.py 2025-01-12 12:39:37 -08:00
SpudGunMan
49b8206e76 Update pong_bot.py 2025-01-12 12:36:08 -08:00
SpudGunMan
5a30cc7511 Update system.py 2025-01-12 12:16:08 -08:00
SpudGunMan
a85cc8c593 Update system.py 2025-01-12 12:09:51 -08:00
SpudGunMan
5ae496702d multiInterfaceRefactors 2025-01-12 11:59:48 -08:00
SpudGunMan
1dffa0987d Update settings.py 2025-01-12 11:47:57 -08:00
SpudGunMan
f3d07eed97 Update README.md 2025-01-12 11:27:29 -08:00
SpudGunMan
de8266b955 Update README.md 2025-01-12 11:19:36 -08:00
SpudGunMan
d482f2ccc9 docker enhancements 2025-01-12 11:19:27 -08:00
SpudGunMan
9f676a4c8d Update entrypoint.sh 2025-01-12 11:07:42 -08:00
SpudGunMan
5d0dae236c Update Dockerfile 2025-01-12 11:03:41 -08:00
SpudGunMan
bf32eca47d Update Dockerfile 2025-01-12 10:45:33 -08:00
SpudGunMan
dcef6da5bc Update Dockerfile 2025-01-12 10:36:34 -08:00
SpudGunMan
a1ffc8b1f6 Update Dockerfile 2025-01-12 10:21:14 -08:00
SpudGunMan
921b66f9e1 Update entrypoint.sh 2025-01-12 10:12:06 -08:00
SpudGunMan
0553a43a01 Update Dockerfile 2025-01-12 10:10:48 -08:00
SpudGunMan
785deb2add add uninstall info
@noon92 👀
2025-01-11 10:09:55 -08:00
SpudGunMan
4b0654971c downgrade this log 2025-01-08 21:51:52 -08:00
SpudGunMan
d2fd133743 extraLocation
@turnrye another thing to check out
2025-01-05 21:50:36 -08:00
SpudGunMan
d689495ee7 Cleanup scripts
note here https://github.com/SpudGunMan/meshing-around/pull/103 and @turnrye can you review this branch and commit
2025-01-05 21:40:20 -08:00
SpudGunMan
b16b4e3c12 Update runShell.sh 2025-01-05 21:35:19 -08:00
SpudGunMan
10109672a7 Update sysEnv.sh 2025-01-05 21:34:05 -08:00
SpudGunMan
4a3cd2560c labCleanupDone 2025-01-05 21:27:25 -08:00
Kelly
576898b8fe Merge pull request #107 from turnrye/docker-compose
Docker compose enhancments
2025-01-05 21:16:41 -08:00
Kelly
4db9c136d6 Lab Cleanup
cleanLab
2025-01-05 21:15:13 -08:00
Kelly
a1a4c1b0f0 Merge branch 'lab2' into lab 2025-01-05 21:14:55 -08:00
Kelly
7b1b435e45 Merge branch 'lab' into docker-compose 2025-01-05 21:06:07 -08:00
SpudGunMan
54e716d2cc enhanceMultiNodeTelemetry 2025-01-05 20:53:30 -08:00
SpudGunMan
b44fa22c11 Update web.py 2025-01-05 20:20:34 -08:00
SpudGunMan
5829cdcef9 reportingEnhance 2025-01-05 20:18:02 -08:00
SpudGunMan
f0a93b0191 Update system.py 2025-01-05 18:24:11 -08:00
SpudGunMan
9014a7e8f9 Update system.py 2025-01-05 18:13:38 -08:00
SpudGunMan
6c9f9f2521 Update config.template 2025-01-05 18:11:57 -08:00
SpudGunMan
9bae30bcb1 Update config.template 2025-01-05 17:42:29 -08:00
SpudGunMan
7069ba1f43 Update system.py 2025-01-05 17:29:00 -08:00
SpudGunMan
ae844f8ecd Update system.py 2025-01-05 17:05:04 -08:00
SpudGunMan
af734ccb1f enhanceSentry 2025-01-05 17:01:07 -08:00
SpudGunMan
1ff5895bad reporting server
@g7kse check this out
2025-01-05 16:39:00 -08:00
SpudGunMan
f12fa0fe9b enhance 2025-01-05 16:20:17 -08:00
SpudGunMan
45c67024e7 enhanceSpotter 2025-01-05 16:19:59 -08:00
SpudGunMan
725cbd8045 Update locationdata.py 2025-01-05 16:01:12 -08:00
SpudGunMan
502a4f2666 Update locationdata.py 2025-01-05 15:37:41 -08:00
SpudGunMan
9aaebaad62 Update locationdata.py 2025-01-05 15:36:37 -08:00
SpudGunMan
d163bffba6 Update locationdata.py 2025-01-05 15:35:22 -08:00
SpudGunMan
36ba04a234 Update locationdata.py 2025-01-05 15:33:24 -08:00
SpudGunMan
0ac683b5c0 Update locationdata.py 2025-01-05 15:33:03 -08:00
SpudGunMan
b16d9322e3 Update system.py 2025-01-05 15:21:20 -08:00
SpudGunMan
868009b650 Update system.py 2025-01-05 15:07:54 -08:00
SpudGunMan
f917df709c refactorWatchDog 2025-01-05 14:58:48 -08:00
SpudGunMan
ab54dc06d7 enhance 2025-01-05 14:02:30 -08:00
SpudGunMan
c7b7b182b9 Update system.py 2025-01-05 13:36:24 -08:00
SpudGunMan
b78cf4d022 Update system.py 2025-01-05 13:18:21 -08:00
SpudGunMan
6f492ef382 interface Expansion 2025-01-05 13:15:54 -08:00
SpudGunMan
e24c9a9d56 Update install.sh 2025-01-05 11:49:37 -08:00
SpudGunMan
b1155dea7d Update install.sh 2025-01-04 21:34:28 -08:00
SpudGunMan
0d9245d448 Update install.sh 2025-01-04 18:49:37 -08:00
SpudGunMan
858bef7703 enhance 2025-01-04 18:48:20 -08:00
Ryan Turner
acf39d0870 fixup! fixup! fixup! fixup! fixup! Initial checkin 2025-01-04 20:40:27 -06:00
Ryan Turner
89a0884600 fixup! fixup! fixup! fixup! Initial checkin 2025-01-04 20:22:40 -06:00
Ryan Turner
70e11117f1 fixup! fixup! fixup! Initial checkin 2025-01-04 20:18:35 -06:00
Ryan Turner
d3f07ae524 fixup! fixup! Initial checkin 2025-01-04 19:58:12 -06:00
Ryan Turner
4f9c36fdad fixup! Initial checkin 2025-01-04 19:41:41 -06:00
Ryan Turner
df15fb54b0 Initial checkin 2025-01-04 19:39:23 -06:00
SpudGunMan
638dc4df16 Update install.sh 2025-01-04 12:54:45 -08:00
SpudGunMan
81e91ab6c5 Update install.sh 2025-01-04 12:53:50 -08:00
SpudGunMan
05476c2bff Update install.sh 2025-01-04 12:51:04 -08:00
SpudGunMan
3b4b0e8c32 Update install.sh 2025-01-04 00:04:57 -08:00
SpudGunMan
772218d108 Update install.sh 2025-01-04 00:04:28 -08:00
SpudGunMan
dae2e4c4f4 enhance embedded 2025-01-03 23:48:44 -08:00
SpudGunMan
5d5595ef8b Update install.sh 2025-01-03 23:42:00 -08:00
SpudGunMan
cf16fc3db7 Update install.sh 2025-01-03 23:39:59 -08:00
SpudGunMan
70659c9c14 Update install.sh 2025-01-03 23:31:08 -08:00
SpudGunMan
b04368f852 location aware
@Ruledo thanks for the idea for this!
2025-01-03 23:11:03 -08:00
SpudGunMan
9e5285a845 Update install.sh 2025-01-03 22:48:41 -08:00
SpudGunMan
475d475e18 Update install.sh 2025-01-03 22:43:56 -08:00
SpudGunMan
2c4cfa9e81 Update install.sh 2025-01-03 22:40:15 -08:00
SpudGunMan
15d7f75507 femtofox butfix
@noon92 this fixes the problem you saw
2025-01-03 22:29:40 -08:00
SpudGunMan
30131bc6d5 Update install.sh 2025-01-02 22:24:42 -08:00
SpudGunMan
5373b61f83 enhance 2025-01-02 22:14:13 -08:00
SpudGunMan
7eb629676b Update install.sh 2025-01-02 22:11:49 -08:00
SpudGunMan
db9b89d0ac Update pong_bot.py 2025-01-02 22:08:59 -08:00
SpudGunMan
d7af337a63 enhance 2025-01-02 22:06:00 -08:00
SpudGunMan
e3c5eb6add logLevel in Config
sysloglevel = DEBUG in config.ini
2025-01-02 21:58:15 -08:00
SpudGunMan
b0e57e8aca cleanup Embedded 2025-01-02 21:57:53 -08:00
SpudGunMan
b4168214b6 #hints 2025-01-02 21:02:14 -08:00
SpudGunMan
7fa5928537 Update README.md 2025-01-02 20:27:50 -08:00
SpudGunMan
f12198b140 enhance 2025-01-02 20:23:04 -08:00
Kelly
0d44ffb635 Merge pull request #101 from joshbowyer/patch-1
Update install.sh enhance stability
2025-01-02 20:00:22 -08:00
joshbowyer
c11ebf1443 Update install.sh
changed if statements to handle user input better
2025-01-02 21:55:09 -06:00
SpudGunMan
b94a5ebd8d POSIX 2025-01-02 19:26:30 -08:00
SpudGunMan
3392d2d5a8 Update install.sh 2025-01-01 11:48:57 -08:00
SpudGunMan
1df3a7aaa2 enhance 2025-01-01 11:35:49 -08:00
SpudGunMan
9a11214208 fix alerting 2024-12-28 09:28:08 -08:00
SpudGunMan
0a4f101370 Update install.sh 2024-12-27 17:22:58 -08:00
SpudGunMan
5f3c32dc00 Update install.sh 2024-12-27 16:50:24 -08:00
SpudGunMan
74cb135c6c Update install.sh
enhance embedded
2024-12-27 16:26:00 -08:00
SpudGunMan
a20e520501 Update install.sh 2024-12-27 14:03:20 -08:00
SpudGunMan
23e0e4c6a0 Update install.sh 2024-12-27 14:03:03 -08:00
SpudGunMan
10918546d6 Update install.sh 2024-12-27 14:01:19 -08:00
SpudGunMan
cf16cc6606 Update install.sh 2024-12-27 13:58:31 -08:00
SpudGunMan
3b73b665d6 Update install.sh 2024-12-27 13:57:02 -08:00
SpudGunMan
993fd760af Update install.sh 2024-12-27 13:55:41 -08:00
SpudGunMan
a029334576 Update install.sh 2024-12-27 13:48:47 -08:00
SpudGunMan
eb8143f298 Update install.sh 2024-12-27 13:36:15 -08:00
SpudGunMan
c756b447ac Update install.sh 2024-12-27 10:28:01 -08:00
SpudGunMan
cef05e061c Update install.sh 2024-12-27 10:04:58 -08:00
SpudGunMan
c85d517b91 Update install.sh 2024-12-27 10:03:13 -08:00
SpudGunMan
170d1a6a45 Update install.sh 2024-12-27 09:52:01 -08:00
SpudGunMan
8d2313cfb1 Update install.sh 2024-12-27 09:49:54 -08:00
SpudGunMan
ed8636f5a5 Update config.template 2024-12-26 16:28:52 -08:00
SpudGunMan
b95d94f06f alertChange 2024-12-26 09:29:30 -08:00
SpudGunMan
f7cdf446bf Update system.py 2024-12-24 18:44:44 -08:00
SpudGunMan
28e8e2705a fix Keyerror 2024-12-24 18:43:11 -08:00
SpudGunMan
9bc6f6f661 Update system.py 2024-12-24 12:21:58 -08:00
SpudGunMan
2630310210 Update system.py 2024-12-24 11:29:54 -08:00
SpudGunMan
3fae42305c sysEnv enhance 2024-12-23 18:56:05 -08:00
SpudGunMan
9cc8dd7143 Update runShell.sh 2024-12-23 16:36:33 -08:00
SpudGunMan
7ffa9d5309 Update mesh_bot.py 2024-12-23 13:17:06 -08:00
SpudGunMan
30d2b996c0 Update filemon.py 2024-12-23 13:16:11 -08:00
SpudGunMan
49c098ef0b Update filemon.py 2024-12-23 13:15:53 -08:00
SpudGunMan
afa41c6ecd Update runShell.sh 2024-12-23 13:12:06 -08:00
SpudGunMan
8861179cb2 Update runShell.sh 2024-12-23 13:11:47 -08:00
SpudGunMan
f32ceb0383 Update runShell.sh 2024-12-23 13:11:09 -08:00
SpudGunMan
9a380964aa Update runShell.sh 2024-12-23 13:09:46 -08:00
SpudGunMan
180a8261ca enhance 2024-12-23 12:55:29 -08:00
SpudGunMan
0536657c8e Update config.template 2024-12-23 12:52:17 -08:00
Kelly
c5a2330dd1 Merge pull request #98 from SpudGunMan/lab
external bash script access
2024-12-23 12:24:54 -08:00
SpudGunMan
dc0b5be387 Update README.md 2024-12-23 12:22:23 -08:00
SpudGunMan
a1f43a5e94 Update runShell.sh 2024-12-23 12:19:40 -08:00
SpudGunMan
b05a817769 Update runShell.sh 2024-12-23 12:18:26 -08:00
SpudGunMan
f7187fdf27 Update runShell.sh 2024-12-23 12:16:05 -08:00
SpudGunMan
cca51d68dd Update locationdata.py 2024-12-23 12:10:03 -08:00
SpudGunMan
21804cc975 scriptingEnhancment 2024-12-23 12:08:28 -08:00
SpudGunMan
7a9ee27336 Update filemon.py 2024-12-23 02:55:57 -08:00
SpudGunMan
0c637226b2 Update config.template 2024-12-22 20:31:13 -08:00
SpudGunMan
555b14ddc0 enhance🐝 2024-12-22 02:55:10 -08:00
SpudGunMan
656c23c631 Update system.py 2024-12-22 01:16:09 -08:00
SpudGunMan
bb591257c9 Update README.md 2024-12-22 00:13:49 -08:00
SpudGunMan
364a5c5c67 🐝
the b can be for the movie or a bible or any other fun idea. be kind.
2024-12-22 00:11:35 -08:00
SpudGunMan
8cb05d38db Update system.py 2024-12-20 21:26:16 -08:00
SpudGunMan
f9fe13f322 Update system.py 2024-12-20 21:22:21 -08:00
Kelly
b8d33cc270 Merge pull request #97 from SpudGunMan/emergencyalert
EnhanceEmergencyAlert
2024-12-20 14:39:31 -08:00
SpudGunMan
a6ce9e9211 remove Numpy 2024-12-20 12:22:01 -08:00
SpudGunMan
60bdabdd1b embedded 2024-12-20 02:00:36 -08:00
SpudGunMan
9c5c2080cf Update locationdata_eu.py 2024-12-20 01:17:47 -08:00
SpudGunMan
8f758229cb Update install.sh 2024-12-20 00:38:59 -08:00
SpudGunMan
8ac9c53f1a enhance groupPing 2024-12-19 18:29:35 -08:00
SpudGunMan
98cbf5528c fixEmbedded 2024-12-19 17:46:26 -08:00
SpudGunMan
6296150677 Update pong_bot.py 2024-12-19 17:40:10 -08:00
SpudGunMan
13cb1e8df9 Update mesh_bot.py 2024-12-19 17:39:15 -08:00
SpudGunMan
e26e876ccf Update system.py 2024-12-19 17:21:33 -08:00
SpudGunMan
550b50f74e Update settings.py 2024-12-19 17:06:24 -08:00
SpudGunMan
ac5aa1a201 Update system.py 2024-12-19 17:03:53 -08:00
SpudGunMan
19700f54c5 Update system.py 2024-12-19 16:55:26 -08:00
SpudGunMan
7e5626cd30 Update system.py 2024-12-19 16:27:09 -08:00
SpudGunMan
c27b6ed8a1 enhanceEmergency Alerting 2024-12-19 16:18:38 -08:00
SpudGunMan
717181bcd0 Update locationdata_eu.py 2024-12-19 16:07:07 -08:00
SpudGunMan
4d5916df29 Update settings.py 2024-12-18 19:58:34 -08:00
SpudGunMan
93b7a1d613 enableGBalerts 2024-12-18 19:58:21 -08:00
SpudGunMan
35cc029984 Update README.md 2024-12-18 19:54:47 -08:00
SpudGunMan
589d44c152 Update locationdata_eu.py 2024-12-18 19:52:49 -08:00
SpudGunMan
06a14d875f enableUKalerts 2024-12-18 19:39:55 -08:00
SpudGunMan
454f823ad7 england.GovAlert 2024-12-18 19:33:11 -08:00
SpudGunMan
6974c4ef66 Update locationdata_eu.py 2024-12-18 19:24:33 -08:00
SpudGunMan
bd956dfebc locationEnhance 2024-12-18 15:09:38 -08:00
SpudGunMan
4aaac5ba49 Update README.md 2024-12-18 13:57:58 -08:00
SpudGunMan
2ae792dd8d Update README.md 2024-12-18 10:55:53 -08:00
SpudGunMan
ca033f024e enhanceNews
returns a random line from the file
2024-12-18 10:53:44 -08:00
SpudGunMan
ad11f787de Update locationdata_eu.py 2024-12-17 23:12:52 -08:00
SpudGunMan
e3d1607c86 enhance EU 2024-12-17 23:11:05 -08:00
SpudGunMan
b68461cbc8 move the moon 2024-12-17 22:57:14 -08:00
SpudGunMan
ddad35aa1e Update README.md 2024-12-17 22:42:15 -08:00
SpudGunMan
35f4aad6f8 riverFlow 2024-12-17 22:16:02 -08:00
SpudGunMan
f08f98e040 Update locationdata.py 2024-12-17 21:55:12 -08:00
SpudGunMan
467376d9c7 Update mesh_bot.py 2024-12-17 21:47:05 -08:00
SpudGunMan
1cbdc93632 riverFlowAlpha 2024-12-17 20:32:07 -08:00
SpudGunMan
2323015617 riverFlood 2024-12-17 20:06:16 -08:00
SpudGunMan
51de0dee8a riverFlow 2024-12-17 13:32:08 -08:00
SpudGunMan
b74c0ebd36 Update wx_meteo.py 2024-12-17 13:29:15 -08:00
SpudGunMan
0a4c54a5a2 Update locationdata.py 2024-12-17 12:14:00 -08:00
SpudGunMan
481809493c Update wx_meteo.py 2024-12-16 20:56:28 -08:00
SpudGunMan
c3914e0423 Update mesh_bot.py 2024-12-16 20:54:57 -08:00
SpudGunMan
ac40254bc4 refactor Openmeteo wx
eliminate requirement for modules and use requests native
2024-12-16 20:43:52 -08:00
SpudGunMan
b6540a1d20 🚨improve EAS duplicates 2024-12-16 09:05:59 -08:00
Kelly
87d29d123f Merge pull request #96 from todd2982/patch-1 2024-12-16 08:05:24 -08:00
todd2982
0aa6f8cc07 Patch CVE found in base python image
Patches the following CVE:
CVE-2024-6345
CVE-2023-5752
2024-12-16 08:21:01 -06:00
SpudGunMan
e2bb480f5f output fix femtofox
Python 3.10.12 had issues
2024-12-15 01:04:34 -08:00
SpudGunMan
920f951e47 Update Dockerfile 2024-12-14 23:00:18 -08:00
SpudGunMan
215fe76f2a CodeQLBadge 2024-12-13 23:27:14 -08:00
SpudGunMan
1740bbf666 Update install.sh 2024-12-13 21:35:10 -08:00
SpudGunMan
f9370d47b4 Update install.sh 2024-12-13 21:34:01 -08:00
SpudGunMan
91072cb47d Update install.sh 2024-12-13 21:29:09 -08:00
SpudGunMan
c30be37f02 femtofox 2024-12-13 21:27:35 -08:00
SpudGunMan
d51dadba04 Update install.sh 2024-12-13 21:20:57 -08:00
SpudGunMan
99c404f479 moveThisShakeThat 2024-12-13 20:12:40 -08:00
SpudGunMan
659ee2959c cleanup 2024-12-13 20:10:59 -08:00
SpudGunMan
1ac9f3b0d6 loop detector 2024-12-13 20:04:20 -08:00
SpudGunMan
d0dc737863 Update README.md 2024-12-13 14:57:14 -08:00
SpudGunMan
e438c82a11 enhance 2024-12-13 14:19:22 -08:00
SpudGunMan
9d7d4601dc Update system.py 2024-12-13 13:29:10 -08:00
SpudGunMan
fdd741446c Update system.py 2024-12-13 13:15:11 -08:00
SpudGunMan
fdbab1685f Update locationdata.py 2024-12-13 13:06:05 -08:00
SpudGunMan
ed0940b126 🧀 2024-12-13 13:03:41 -08:00
SpudGunMan
a087c7bb3a Update system.py 2024-12-13 13:02:06 -08:00
SpudGunMan
0439db2ec0 sysinfo
returns telemetry info
2024-12-13 12:59:12 -08:00
SpudGunMan
c1a5d4d336 Create gpio.py 2024-12-13 12:30:49 -08:00
SpudGunMan
eeffc6361a enhance@🏓 2024-12-13 11:45:50 -08:00
SpudGunMan
e2be3c20b7 enhance🏓 2024-12-13 10:30:18 -08:00
SpudGunMan
b43c21fc98 emergency responder block list 2024-12-13 10:22:33 -08:00
SpudGunMan
e115f33d47 pingEnhancments🏓
added autoPingInChannel = False
 # Allows auto-ping feature in a channel, False forces DM

also added node short name to all channel ping
2024-12-13 10:07:14 -08:00
SpudGunMan
b8016aafc9 Update mesh_bot.py 2024-12-12 23:53:11 -08:00
SpudGunMan
743b0ab10b Update mesh_bot.py 2024-12-12 23:49:44 -08:00
SpudGunMan
e06b2a3581 Update mesh_bot.py 2024-12-12 23:45:02 -08:00
SpudGunMan
582e00402a enhance satpass for user list 2024-12-12 23:36:10 -08:00
SpudGunMan
82551e0b4a Update space.py 2024-12-12 22:50:13 -08:00
SpudGunMan
a9c2660ec1 MQTT Logic for ping 2024-12-12 22:44:24 -08:00
SpudGunMan
fa802ba313 Update README.md 2024-12-12 22:16:08 -08:00
SpudGunMan
874d56045e Update README.md 2024-12-12 22:15:56 -08:00
SpudGunMan
8204cbe60f Update README.md 2024-12-12 22:14:00 -08:00
SpudGunMan
a50c06206c Update locationdata.py 2024-12-12 14:48:35 -08:00
SpudGunMan
895e5a2b07 typos 2024-12-12 14:45:15 -08:00
SpudGunMan
2012986aff Update locationdata.py 2024-12-12 14:25:46 -08:00
SpudGunMan
63d1f84887 Update system.py 2024-12-12 14:25:40 -08:00
SpudGunMan
d8233bc9e2 Update locationdata.py 2024-12-12 12:55:27 -08:00
SpudGunMan
bdea3d6036 Update install.sh 2024-12-12 12:26:35 -08:00
SpudGunMan
2fe2009b97 Update install.sh 2024-12-12 12:12:59 -08:00
SpudGunMan
dcad12935f Update install.sh 2024-12-12 12:11:09 -08:00
SpudGunMan
0e2f6343a2 Update install.sh 2024-12-12 12:03:26 -08:00
SpudGunMan
56bd6f9ea7 iPAWS pin
if you have a PIN otherwise ignore this
2024-12-12 11:49:14 -08:00
SpudGunMan
5718a43d20 Update locationdata.py 2024-12-12 11:42:58 -08:00
SpudGunMan
f759e2e7e5 Update locationdata.py 2024-12-12 11:26:34 -08:00
SpudGunMan
1e97554cbf Update locationdata.py 2024-12-12 11:25:51 -08:00
SpudGunMan
04d4a2f5a7 Update locationdata.py 2024-12-12 11:22:23 -08:00
SpudGunMan
fb47756deb Update locationdata.py 2024-12-12 11:15:13 -08:00
SpudGunMan
a33fed711d Update install.sh 2024-12-12 11:12:13 -08:00
SpudGunMan
bcb741102d Update install.sh 2024-12-12 11:06:29 -08:00
SpudGunMan
8b2d933fd1 Update install.sh 2024-12-12 11:06:17 -08:00
SpudGunMan
f8d6419551 Update install.sh 2024-12-12 11:05:03 -08:00
SpudGunMan
cf518aeff5 Update install.sh 2024-12-12 11:00:04 -08:00
SpudGunMan
95eebcde2b Update install.sh 2024-12-12 10:49:31 -08:00
SpudGunMan
5cd7dca9b0 Update install.sh 2024-12-12 10:47:53 -08:00
SpudGunMan
eb87cf1bc8 Update install.sh 2024-12-12 10:45:09 -08:00
SpudGunMan
8a510a7b11 Update install.sh 2024-12-12 10:38:09 -08:00
SpudGunMan
e2631407e8 Update install.sh 2024-12-12 10:35:38 -08:00
SpudGunMan
eb86fa911c Update README.md 2024-12-12 10:30:39 -08:00
SpudGunMan
448ad65c67 Update install.sh 2024-12-12 10:15:25 -08:00
SpudGunMan
bb8d2167ce Update install.sh 2024-12-12 10:14:31 -08:00
SpudGunMan
a2bf33d71d Update install.sh 2024-12-12 10:08:19 -08:00
SpudGunMan
e287bdeaef Update system.py 2024-12-12 03:01:45 -08:00
SpudGunMan
16e5acbd27 Update README.md 2024-12-12 02:25:29 -08:00
SpudGunMan
1ea6961393 🛰️satpass
get the next passes needs a API key
2024-12-12 02:14:26 -08:00
SpudGunMan
bd2bce0029 Update mesh_bot.py 2024-12-11 21:57:36 -08:00
SpudGunMan
33c8d4c0ad Update mesh_bot.py 2024-12-11 21:14:46 -08:00
SpudGunMan
d453c3cac1 Update README.md 2024-12-11 20:33:23 -08:00
SpudGunMan
187fc7c2e4 Update README.md 2024-12-11 20:32:21 -08:00
SpudGunMan
33154626e5 enhance CMD Help 2024-12-11 20:07:01 -08:00
SpudGunMan
cfdbf1836f Update system.py 2024-12-11 19:52:03 -08:00
SpudGunMan
054692adf0 whois
🦉
2024-12-11 19:50:24 -08:00
SpudGunMan
ce33421b16 Update launch.sh 2024-12-11 17:14:38 -08:00
SpudGunMan
d2cde424fc Update install.sh 2024-12-11 17:14:32 -08:00
SpudGunMan
517ae5d4b4 Update entrypoint.sh 2024-12-11 17:14:28 -08:00
SpudGunMan
e69ee5c1a8 Update simulator.py 2024-12-11 17:08:11 -08:00
SpudGunMan
b2eae85cc2 ea and ealert
It's in the Bot, not a game, its FEMA. 🦺
2024-12-11 16:15:40 -08:00
SpudGunMan
0749df04e5 Update locationdata.py 2024-12-11 15:38:34 -08:00
SpudGunMan
a66ea58d24 Update config.template 2024-12-11 14:45:48 -08:00
SpudGunMan
13738d1042 Update README.md 2024-12-11 14:43:02 -08:00
SpudGunMan
695d510b9f Update config.template 2024-12-11 14:42:35 -08:00
SpudGunMan
f5e80c31b1 Update README.md 2024-12-11 13:15:00 -08:00
SpudGunMan
572a15fbab Update config.template 2024-12-11 13:03:10 -08:00
SpudGunMan
8dc9a5de3f enhanceing ipaws
pin allowance be it hidden still
better SAME detector
alert test ignore
2024-12-11 12:57:48 -08:00
SpudGunMan
c8643b7ce9 Update README.md 2024-12-11 12:24:54 -08:00
SpudGunMan
786dcab420 ealert live for testing
progress on https://github.com/SpudGunMan/meshing-around/issues/90
2024-12-10 18:01:38 -08:00
SpudGunMan
ab2f9a9846 FEMA config.ini 2024-12-10 17:58:25 -08:00
SpudGunMan
daf43f306b ealert FEMA alerting
this code is still very early
2024-12-10 17:48:40 -08:00
SpudGunMan
53adb4be70 Update locationdata.py 2024-12-10 17:22:35 -08:00
SpudGunMan
2458a4d141 Update locationdata.py 2024-12-10 17:14:22 -08:00
SpudGunMan
1c78a8f593 Update locationdata.py 2024-12-10 16:59:48 -08:00
SpudGunMan
6077eef26e change to abbreviate_weather 2024-12-10 16:54:23 -08:00
SpudGunMan
8f3aaaba25 Update locationdata.py 2024-12-10 16:26:11 -08:00
SpudGunMan
b1cd0ca44f Update locationdata.py 2024-12-10 16:24:08 -08:00
SpudGunMan
879555915f Update locationdata.py 2024-12-10 16:17:02 -08:00
SpudGunMan
f61ba7c1af testSAME logic 2024-12-10 16:03:42 -08:00
SpudGunMan
7cb2ea33c7 Update locationdata.py
progress on https://github.com/SpudGunMan/meshing-around/issues/90
2024-12-10 15:33:43 -08:00
SpudGunMan
855a9ac0d0 Update locationdata.py 2024-12-10 15:28:48 -08:00
SpudGunMan
3e2e1de8ce Update locationdata.py
getting functional iPAWS
2024-12-10 15:01:53 -08:00
SpudGunMan
372f49d6ef Update config.template 2024-12-10 14:11:19 -08:00
Kelly
a31b3e1c79 Merge pull request #92 from turnrye/patch-1
Fix typo on README
2024-12-10 14:04:05 -08:00
SpudGunMan
d3ecef9216 Update config.template 2024-12-10 02:24:56 -08:00
SpudGunMan
1175e23525 Update smtp.py 2024-12-09 21:56:35 -08:00
SpudGunMan
08e3e21306 Update smtp.py 2024-12-09 21:56:09 -08:00
SpudGunMan
7e3de5e490 Update mesh_bot.py 2024-12-09 21:53:45 -08:00
SpudGunMan
abc3eccf4e Update smtp.py 2024-12-09 21:53:41 -08:00
SpudGunMan
80751f9cfc Update README.md 2024-12-09 21:40:20 -08:00
SpudGunMan
7209992887 Update README.md 2024-12-09 21:38:51 -08:00
SpudGunMan
6c18d97f27 Update README.md 2024-12-09 20:35:19 -08:00
SpudGunMan
cdd7d6e766 Update README.md 2024-12-09 20:32:31 -08:00
SpudGunMan
8d5334126f emailSentryAlerts 2024-12-09 20:29:16 -08:00
SpudGunMan
bcd23ebb83 Update smtp.py 2024-12-09 20:15:04 -08:00
SpudGunMan
5d581c2319 Update README.md 2024-12-09 20:14:38 -08:00
SpudGunMan
8e3b449c42 Update smtp.py 2024-12-09 20:11:08 -08:00
SpudGunMan
0975b3235a Update smtp.py 2024-12-09 20:03:56 -08:00
SpudGunMan
9a2a4f1b77 enhanceSMTP 2024-12-09 19:55:00 -08:00
SpudGunMan
5df17b5905 fixSMTPAuth 2024-12-09 19:45:03 -08:00
SpudGunMan
894c5f155f Update README.md 2024-12-09 19:20:51 -08:00
SpudGunMan
f848e12571 Update smtp.py 2024-12-09 19:10:51 -08:00
SpudGunMan
7adf6e7a1d Update smtp.py 2024-12-09 19:06:47 -08:00
SpudGunMan
c6958c7c69 Update smtp.py 2024-12-09 18:55:08 -08:00
Ryan Turner
05c6e56a4f Fix typo on README 2024-12-09 20:00:56 -06:00
SpudGunMan
c45cf5d207 Update smtp.py 2024-12-09 17:52:12 -08:00
SpudGunMan
a3995f7cce Update mesh_bot.py 2024-12-09 17:51:09 -08:00
SpudGunMan
fb3652a954 Update smtp.py 2024-12-09 17:51:03 -08:00
SpudGunMan
b385001db2 hopDebug 2024-12-09 16:10:22 -08:00
SpudGunMan
ac96ca9e2f Update mesh_bot.py 2024-12-09 15:07:02 -08:00
SpudGunMan
02ffe0eb3a Update mesh_bot.py 2024-12-09 14:56:32 -08:00
SpudGunMan
389945e023 Update smtp.py 2024-12-09 14:47:53 -08:00
SpudGunMan
446fa0c049 timeout 2024-12-09 14:27:04 -08:00
SpudGunMan
8b4409c115 Update smtp.py 2024-12-09 14:19:14 -08:00
SpudGunMan
5684a75c65 Update smtp.py 2024-12-09 14:13:05 -08:00
SpudGunMan
1c6a98fea5 Update smtp.py 2024-12-09 14:08:52 -08:00
SpudGunMan
7c1b886c3d Update README.md 2024-12-09 13:13:30 -08:00
SpudGunMan
75bbd1a0cd enhanceWecomeMessage
When there is no LLM the meshbot will now respond with more IQ

also added the awareness of some extra bits
and added a new tracker for all nodes seenNodes
2024-12-09 12:37:31 -08:00
SpudGunMan
a53f5a033b Update mesh_bot.py 2024-12-09 00:27:44 -08:00
SpudGunMan
ea37405149 Update mesh_bot.py 2024-12-08 19:48:54 -08:00
SpudGunMan
e16ecbe1b7 Update README.md 2024-12-08 19:44:51 -08:00
SpudGunMan
db6f20dd3b SMTPConfig 2024-12-08 19:38:58 -08:00
SpudGunMan
9fa60d0c84 Update config.template 2024-12-08 19:27:10 -08:00
SpudGunMan
2fdad79dbb SMTP module work 2024-12-08 19:23:54 -08:00
SpudGunMan
20342fb58c Update smtp.py 2024-12-08 14:39:50 -08:00
SpudGunMan
b7e815cf85 Update smtp.py 2024-12-08 14:36:55 -08:00
SpudGunMan
8e3d1c432e Update smtp.py 2024-12-08 14:34:53 -08:00
SpudGunMan
1a8ed573a8 Update smtp.py 2024-12-08 14:34:04 -08:00
SpudGunMan
63516b36e4 sentMultipleSMS 2024-12-08 14:33:01 -08:00
SpudGunMan
d17b05a40a Update smtp.py 2024-12-08 14:29:26 -08:00
SpudGunMan
e4cefa2264 Update smtp.py 2024-12-08 14:28:22 -08:00
SpudGunMan
90bf3459c9 typeSetCommands 2024-12-08 14:21:51 -08:00
SpudGunMan
0983259117 banList 2024-12-08 14:16:45 -08:00
SpudGunMan
377e5a9825 multipleSMS
clearSMS
2024-12-08 14:11:43 -08:00
SpudGunMan
7edcb4457a Update smtp.py 2024-12-08 13:51:23 -08:00
SpudGunMan
3fec7867d9 Update filemon.py 2024-12-08 13:28:23 -08:00
SpudGunMan
7e447616d9 IMAP
all untested still
2024-12-08 13:06:56 -08:00
SpudGunMan
e59c3de0aa Update smtp.py 2024-12-08 12:54:09 -08:00
SpudGunMan
db808568cb Update settings.py 2024-12-08 11:32:54 -08:00
SpudGunMan
0615733445 enhance emergencyResponder 2024-12-08 11:32:30 -08:00
SpudGunMan
402c58c111 emergencyResponder 2024-12-08 11:27:34 -08:00
SpudGunMan
dde6c2ed32 Update README.md 2024-12-08 09:46:45 -08:00
SpudGunMan
766ff0a195 SMTP Module
inital idea
2024-12-07 21:18:46 -08:00
SpudGunMan
d614cbcff5 Update README.md 2024-12-07 16:06:05 -08:00
SpudGunMan
81798c1fc2 Update locationdata.py 2024-12-07 15:11:05 -08:00
SpudGunMan
210a75671f FEMAipaws
not functioning yet
2024-12-07 14:50:25 -08:00
SpudGunMan
f3e113dcc1 Update README.md 2024-12-07 00:27:24 -08:00
SpudGunMan
145664a42f Update llm.py 2024-12-07 00:04:01 -08:00
SpudGunMan
acc770732e Update config.template 2024-12-07 00:03:52 -08:00
Kelly
ded4c79911 Merge pull request #87 from propstg/game-fixes 2024-12-06 23:26:59 -08:00
propstg
ad0c9c710f Add error message when trying to buy max when inventory full, instead of sending usage message 2024-12-07 01:54:47 -05:00
propstg
259c4991f9 Show name property instead of object's tostring 2024-12-07 01:20:00 -05:00
SpudGunMan
5fe185ab7f Update README.md 2024-12-06 13:42:16 -08:00
SpudGunMan
974caaff42 Update system.py 2024-12-06 13:18:52 -08:00
SpudGunMan
41d8758969 enhanceChunkr
better logic?
2024-12-06 12:59:46 -08:00
SpudGunMan
92e1e3168e Update pong_bot.py 2024-12-06 11:35:06 -08:00
SpudGunMan
a608e29911 Update mesh_bot.py 2024-12-06 11:34:57 -08:00
SpudGunMan
015b72c8c6 Update requirements.txt 2024-12-06 11:14:56 -08:00
SpudGunMan
74cf5841ff Update README.md 2024-12-06 11:14:40 -08:00
SpudGunMan
9ba7b1c972 remove Ollama Requirement
remove and replace with API call to web only
2024-12-06 10:09:07 -08:00
SpudGunMan
5bf0417203 Update README.md 2024-12-05 23:23:50 -08:00
SpudGunMan
2b7a20f8d9 Update pong_bot.py 2024-12-05 23:19:55 -08:00
SpudGunMan
2afb49cbc7 Update README.md 2024-12-05 23:16:18 -08:00
SpudGunMan
17008b7711 Update README.md 2024-12-05 23:14:36 -08:00
SpudGunMan
36ff328380 Update eas_alert_parser.py 2024-12-05 17:18:48 -08:00
SpudGunMan
bb051f4225 errata 2024-12-05 12:49:29 -08:00
SpudGunMan
61c5be1a08 throttleEAS 2024-12-05 11:39:36 -08:00
SpudGunMan
bc7d47b2a7 Update README.md 2024-12-05 11:32:46 -08:00
SpudGunMan
24bcd5cbf9 readNews
on demand return of the file news.txt for readnews onair
2024-12-05 11:08:11 -08:00
SpudGunMan
8407512b0f fineTuneEAS 2024-12-04 23:31:29 -08:00
SpudGunMan
6f4e8615a3 Update system.py 2024-12-04 20:06:33 -08:00
SpudGunMan
314d36e0dc fixReporter 2024-12-04 15:28:18 -08:00
SpudGunMan
27accb0d4a Update bbstools.py 2024-12-02 16:33:30 -08:00
SpudGunMan
fd84505ad1 typo 2024-12-02 16:25:48 -08:00
SpudGunMan
8f75b13c4d Update mesh_bot.py 2024-12-02 16:23:35 -08:00
SpudGunMan
31d05f8aa7 Update mesh_bot.py 2024-12-02 16:21:40 -08:00
SpudGunMan
cdfe4bb844 Update bbstools.py 2024-12-02 16:14:40 -08:00
SpudGunMan
f30e9cd8b8 Update bbstools.py 2024-12-02 16:14:01 -08:00
SpudGunMan
931bc7b9f7 Update bbstools.py 2024-12-02 16:08:22 -08:00
SpudGunMan
049c0d5ad7 bbsLinkEnhancments 2024-12-02 16:05:14 -08:00
SpudGunMan
a5f1e452e4 Update bbstools.py 2024-12-01 13:13:20 -08:00
SpudGunMan
d89cd8598d limitAutoPing 2024-11-29 20:18:52 -08:00
SpudGunMan
d4e3ea60e3 Update settings.py 2024-11-29 18:23:22 -08:00
SpudGunMan
b98bc8429a Update mesh_bot.py 2024-11-29 18:17:17 -08:00
SpudGunMan
4bb7c9296a Update README.md 2024-11-29 18:06:42 -08:00
SpudGunMan
bb7b5b1c90 scheduler work 2024-11-29 18:05:07 -08:00
SpudGunMan
c400f6f998 Update README.md 2024-11-29 17:27:00 -08:00
SpudGunMan
fce6c0b2e4 Update mesh_bot.py 2024-11-29 17:13:30 -08:00
SpudGunMan
0d0288ba18 Update mesh_bot.py 2024-11-29 17:11:26 -08:00
SpudGunMan
c25d7bc8de Update mesh_bot.py 2024-11-29 17:07:00 -08:00
SpudGunMan
d42fa72d54 fix bbslink/ack 2024-11-29 17:05:28 -08:00
SpudGunMan
bc7176c1cf Update README.md 2024-11-29 11:23:57 -08:00
SpudGunMan
15d454f93a Update eas_alert_parser.py 2024-11-29 10:37:52 -08:00
SpudGunMan
249ee3bb5a Update README.md 2024-11-29 00:27:38 -08:00
SpudGunMan
a3b3d4ea0e Update mesh_bot.py 2024-11-28 23:28:42 -08:00
SpudGunMan
27f9d04538 Update system.py 2024-11-28 23:02:16 -08:00
SpudGunMan
03f1869b23 Update mesh_bot.py 2024-11-28 22:59:49 -08:00
SpudGunMan
479e177a64 exceed the maxBuffer fix 2024-11-28 22:24:41 -08:00
SpudGunMan
5cf166af87 Update mesh_bot.py 2024-11-28 22:06:07 -08:00
SpudGunMan
e24bcd7d38 Update mesh_bot.py 2024-11-28 21:43:03 -08:00
SpudGunMan
768898df64 Update system.py 2024-11-28 21:38:37 -08:00
SpudGunMan
cf282e04bb Update system.py 2024-11-28 21:16:09 -08:00
SpudGunMan
db4edac083 enhance maBuffer Logic 2024-11-28 21:14:41 -08:00
SpudGunMan
877d0cf7f8 enhance MaxBuffer Test
sets the lower limit to 150
2024-11-28 19:35:18 -08:00
SpudGunMan
e78c441a6e Update README.md 2024-11-28 17:23:09 -08:00
SpudGunMan
e945819365 Update mesh_bot.py 2024-11-28 17:12:29 -08:00
SpudGunMan
23e8db50fd Update README.md 2024-11-28 17:11:04 -08:00
SpudGunMan
193ffe6394 Update system.py 2024-11-28 16:53:55 -08:00
SpudGunMan
87016186d8 Update system.py 2024-11-28 16:53:04 -08:00
SpudGunMan
d7d96a89cf Update system.py 2024-11-28 16:51:58 -08:00
SpudGunMan
aa5ef23363 maxBuffer
test 4 will divide the maxBuffer value and send junk data to test a radio or network
2024-11-28 16:41:49 -08:00
SpudGunMan
c18e0401e4 auto TEST Buffer 2024-11-28 16:17:57 -08:00
SpudGunMan
8568990295 Update system.py 2024-11-28 15:55:44 -08:00
SpudGunMan
44e6460224 Update mesh_bot.py 2024-11-28 15:28:58 -08:00
SpudGunMan
d53480290c wantAck 2024-11-28 14:56:49 -08:00
SpudGunMan
1499d883bc dadJokes🥔🚫 2024-11-28 13:20:30 -08:00
SpudGunMan
883a6902fa Update locationdata.py 2024-11-28 13:06:28 -08:00
SpudGunMan
6d3b754c6c bugFix 2024-11-28 00:02:26 -08:00
SpudGunMan
62f73ce2e6 Update README.md 2024-11-27 23:17:05 -08:00
39 changed files with 2988 additions and 1029 deletions

View File

@@ -1,24 +1,21 @@
FROM python:3.10-slim
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/*
# Set the locale default to en_US.UTF-8
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANG="en_US.UTF-8"
ENV TZ="America/Los_Angeles"
WORKDIR /app
COPY . /app
COPY requirements.txt .
COPY config.template /app/config.ini
RUN pip install -r requirements.txt
COPY . .
COPY config.ini /app/config.ini
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/script/docker/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]

316
README.md
View File

@@ -1,14 +1,20 @@
# Mesh Bot for Network Testing and BBS Activities
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
![Example Use](etc/pong-bot.jpg "Example Use")
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
### Intelligent Keyword Responder
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
### 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
### Dual Radio/Node Support
- **Simultaneous Monitoring**: Monitor two networks at the same time.
@@ -19,12 +25,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach
- **BBS Linking**: Combine multiple bots to expand BBS reach.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **Wiki Integration**: Look up data using Wikipedia results.
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
- **Satalite Pass Info**: Get passes for satalite at your location.
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
@@ -37,12 +45,15 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
### NOAA EAS Alerts
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
- **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
### File Monitor Alerts
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
- **News File**: On request of news, the contents of the file are returned.
### Data Reporting
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
@@ -51,33 +62,23 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
## Getting Started
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware.
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), 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
#### Clone the Repository
If you dont have git you will need it `sudo apt-get install git`
```sh
git clone https://github.com/spudgunman/meshing-around
```
The code is under active development, so make sure to pull the latest changes regularly!
#### Optional Automation of setup
#### 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
#### Docker Installation
If you prefer to use Docker, follow these steps:
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
2. Build the Docker image:
```sh
cd meshing-around
docker build -t meshing-around .
```
3. Run the Docker container:
```sh
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
```
If you prefer to use [Docker](script/docker/README.md)
#### Custom Install
Install the required dependencies using pip:
@@ -117,16 +118,17 @@ enabled = False
```
### General Settings
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
```ini
[general]
respond_by_dm_only = True
defaultChannel = 0
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
```
### Location Settings
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
```ini
[location]
@@ -134,10 +136,11 @@ enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
```
### Module Settings
Modules can be enabled or disabled as needed.
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
```ini
[bbs]
@@ -158,18 +161,70 @@ lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
### Sentry Settings
Sentry Bot detects anyone coming close to the bot-node.
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
```ini
SentryEnabled = True # detect anyone close to the bot
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
```
### E-Mail / SMS Settings
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
```ini
[smtp]
# enable or disable the SMTP module, minimum required for outbound notifications
enableSMTP = True # enable or disable the IMAP module for inbound email, not implimented yet
enableImap = False # list of Sysop Emails seperate with commas, used only in emergemcy responder currently
sysopEmails =
# See config.template for all the SMTP settings
SMTP_SERVER = smtp.gmail.com
SMTP_AUTH = True
EMAIL_SUBJECT = Meshtastic✉
```
### Emergency Response Handler
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
```ini
[emergencyHandler]
enabled = True # enable or disable the emergency response handler
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
alert_interface = 1
```
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS and 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
```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
```
#### 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
```
### Repeater Settings
A repeater function for two different nodes and cross-posting messages. The [`repeater_channels`] is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
```ini
[repeater] # repeater module
@@ -177,22 +232,8 @@ enabled = True
repeater_channels = [2, 3]
```
### Radio Monitoring
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```ini
[radioMon]
enabled = False
rigControlServerAddress = localhost:4532
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
signalHoldTime = 10 # hold time for high SNR
signalCooldown = 5 # the following are combined to reset the monitor
signalCycleLimit = 5
```
### Ollama (LLM/AI) Settings
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
```ini
# Enable ollama LLM see more at https://ollama.com
@@ -209,31 +250,47 @@ llmEnableHistory = True # enable history for the LLM model to use in responses a
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
### Radio Monitoring
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```ini
[radioMon]
enabled = True
rigControlServerAddress = localhost:4532
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
signalHoldTime = 10 # hold time for high SNR
signalCooldown = 5 # the following are combined to reset the monitor
signalCycleLimit = 5
```
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
enabled = True
filemon_enabled = True
file_path = alert.txt
broadcastCh = 2,4
enable_read_news = False
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
enable_runShellCmd = False # enables running of bash commands runShell.sh demo for sysinfo
```
#### NOAA EAS
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
```ini
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2,4
```
#### Offline EAS
To Monitor EAS with no internet connection see the following notes
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
no examples yet for these tools
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
- this can be used with a rtl-sdr to capture alerts
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [dsame3](https://github.com/jamieden/dsame3)
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
@@ -245,8 +302,19 @@ The following example shell command will pipe rtl_sdr to alert.txt
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
#### Newspaper on mesh
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
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
### Scheduler
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
In the config.ini enable the module
```ini
[scheduler]
# enable or disable the scheduler module
enabled = True
```
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.
```python
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
@@ -257,81 +325,49 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
```
#### BBS Link
The scheduler also handles the BBL Link Brodcast message
The scheduler also handles the BBS Link Brodcast message, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
```
```ini
bbslink_enabled = True
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
```
### MQTT Notes
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
### Requirements
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install geopy
pip install schedule
pip install wikipedia
```
For open-meteo use:
```sh
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
For the Ollama LLM:
```sh
pip install ollama
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
~~There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two~~
## Full list of commands for the bot
### Networking
| Command | Description | ✅ Works Off-Grid |
|---------|-------------|-
| `ping`, `ack`, `test` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `sysinfo` | Returns the bot node telemetry info | ✅ |
| `test` | used to test the limits of data transfer `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
### Radio Propagation & Weather Forcasting
| Command | Description | |
|---------|-------------|-------------------
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `solar` | Gives an idea of the x-ray flux | |
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
| `hfcond` | Returns a table of HF solar conditions | |
| `tide` | Returns the local tides (NOAA data source) |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details |
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
| `solar` | Gives an idea of the x-ray flux | |
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `tide` | Returns the local tides (NOAA data source) |
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
### Bulletin Board & Mail
| Command | Description | |
@@ -342,27 +378,32 @@ sudo apt-get install fonts-noto-color-emoji
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
| `sms:` | Send sms-email to multiple address on file | |
| `setemail`| Sets the email for easy communciations | |
| `setsms` | Adds the SMS-Email for quick communications | |
| `clearsms` | Clears all SMS-Emails on file for node | |
### Data Lookup
| Command | Description | |
|---------|-------------|-
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
### 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 | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
# Recognition
@@ -385,9 +426,48 @@ I used ideas and snippets from other responder bots and want to call them out!
- **xdep**: For the reporting tools.
- **Nestpebble**: For new ideas and enhancements.
- **mrpatrick1991**: For Docker configurations.
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
- **Cisien, bitflip, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **WH6GXZ nurse dude**: For bashing on installer
- **Josh**: For more bashing on installer!
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
### Requirements
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install schedule
pip install wikipedia
```
For the Ollama LLM:
```sh
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.

View File

@@ -8,80 +8,93 @@
type = serial
port = /dev/ttyACM0
# port = /dev/ttyUSB0
# port = COM1
# hostname = 192.168.0.1
# hostname = meshtastic.local
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support
# Additional interface for multi radio support
[interface2]
enabled = False
type = serial
port = /dev/ttyUSB0
#port = /dev/ttyACM1
# port = /dev/ttyACM1
# port = COM1
# hostname = meshtastic.local
# hostname = localhost
# mac = 00:11:22:33:44:55
# example, the third interface would be [interface3] up to 9
[general]
# if False will respond on all channels but the default channel
respond_by_dm_only = True
# Allows auto-ping feature in a channel, False forces to 1 ping only
autoPingInChannel = False
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
defaultChannel = 0
# ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreDefaultChannel = False
# motd is reset to this value on boot
motd = Thanks for using MeshBOT! Have a good day!
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
# whoami
whoami = True
# enable or disable the Joke module
DadJokes = True
DadJokesEmoji = False
# enable or disable the Solar module
spaceWeather = True
# enable or disable the wikipedia search module
wikipedia = True
# Enable ollama LLM see more at https://ollama.com
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
# history command
enableCmdHistory = True
# command history ignore list ex: 2813308004,4258675309
lheardCmdIgnoreNodes =
# 24 hour clock
zuluTime = False
# wait time for URL requests
urlTimeout = 10
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
mastermind = True
golfsim = True
[emergencyHandler]
# enable or disable the emergency response handler
enabled = False
# channel to send a message to when the emergency handler is triggered
alert_channel = 2
alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
# channel to send a message to when the watchdog is triggered
SentryChannel = 9
SentryChannel = 2
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
@@ -93,26 +106,57 @@ enabled = True
bbs_ban_list =
# list of admin nodes numbers ex: 2813308004,4258675309
bbs_admin_list =
# enable bbs synchronization with other nodes
bbslink_enabled = False
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
bbslink_whitelist =
# location module
[location]
enabled = True
lat = 48.50
lon = -123.0
# Default to metric units rather than imperial
useMetric = False
# repeaterList lookup location (rbook / artsci)
repeaterLookup = rbook
# NOAA weather forecast days, the first two rows are today and tonight
NOAAforecastDuration = 4
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# Default to metric units rather than imperial
useMetric = False
# repeaterList lookup location (rbook / artsci)
repeaterLookup = rbook
# NOAA Hydrology unique identifiers, LID or USGS ID
riverListDefault =
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# Add extra location to the weather alert
enableExtraLocationWx = False
# Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastEnabled = False
# Goverment Emergency IPAWS/CAP 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
# Satalite Pass Prediction
# Register for free API https://www.n2yo.com/login/
n2yoAPIKey =
# NORAD list https://www.n2yo.com/satellites/
satList = 25544,7530
# repeater module
[repeater]
@@ -121,7 +165,11 @@ enabled = False
# and rebroadcasted on the same channel on the other device/node/interface
# with great power comes great responsibility, danger could be lurking in use of this feature
# if you have the two nodes on the same radio configurations, you could create a feedback loop
repeater_channels =
repeater_channels =
[scheduler]
# enable or disable the scheduler module
enabled = False
[radioMon]
# using Hamlib rig control will monitor and alert on channel use
@@ -138,15 +186,63 @@ signalCooldown = 5
signalCycleLimit = 5
[fileMon]
enabled = False
filemon_enabled = False
file_path = alert.txt
broadcastCh = 2
enable_read_news = False
news_file_path = news.txt
# only return a single random line from the news file
news_random_line = False
# enable the use of exernal shell commands
enable_runShellCmd = False
[smtp]
# enable or disable the SMTP module
enableSMTP = False
# enable or disable the IMAP module for inbound email
enableImap = False
# list of Sysop Emails seperate with commas
sysopEmails =
SMTP_SERVER = smtp.gmail.com
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
SMTP_PORT = 587
# Sender email: be mindful of public access, don't use your personal email
FROM_EMAIL = none@gmail.com
SMTP_AUTH = True
SMTP_USERNAME = none@gmail.com
SMTP_PASSWORD = none
EMAIL_SUBJECT = Meshtastic✉
# IMAP not implimented yet
IMAP_SERVER = imap.gmail.com
# 993 IMAP over TLS/SSL, 143 legacy IMAP
IMAP_PORT = 993
# IMAP login usually same as SMTP
IMAP_USERNAME = none@gmail.com
IMAP_PASSWORD = none
IMAP_FOLDER = inbox
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
mastermind = True
golfsim = True
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7
responseDelay = 1.2
# delay in seconds for splits in messages to avoid message collision
splitDelay = 0.0
# message chunk size for sending at high success rate
# message chunk size for sending at high success rate, chunkr allows exceeding by 3 characters
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
maxBuffer = 220

View File

@@ -1,6 +0,0 @@
#!/bin/bash
# Substitute environment variables in the config file
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
exec python /app/mesh_bot.py

View File

@@ -23,6 +23,27 @@ except Exception as e:
except Exception as e:
bbs_dm = "System: data/bbsdm.pkl not found"
try:
with open('../data/email_db.pickle', 'rb') as f:
email_db = pickle.load(f)
except Exception as e:
try:
with open('data/email_db.pickle', 'rb') as f:
email_db = pickle.load(f)
except Exception as e:
email_db = "System: data/email_db.pickle not found"
try:
with open('../data/sms_db.pickle', 'rb') as f:
sms_db = pickle.load(f)
except Exception as e:
try:
with open('data/sms_db.pickle', 'rb') as f:
sms_db = pickle.load(f)
except Exception as e:
sms_db = "System: data/sms_db.pickle not found"
# Game HS tables
try:
with open('../data/lemonstand.pkl', 'rb') as f:
@@ -90,6 +111,10 @@ print ("System: bbs_messages")
print (bbs_messages)
print ("\nSystem: bbs_dm")
print (bbs_dm)
print ("\nSystem: email_db")
print (email_db)
print ("\nSystem: sms_db")
print (sms_db)
print (f"\n\nGame HS tables\n")
print (f"lemon:{lemon_score}")
print (f"dopewar:{dopewar_score}")

View File

@@ -9,32 +9,41 @@ from EAS2Text import EAS2Text
buff=[] # store messages for writing
seen=set()
pattern = re.compile(r'ZCZC.*?NWS-')
# alternate regex for parsing multimon-ng output
# provided by https://github.com/A-c0rN
#reg = r"^.*?(NNNN|ZCZC)(?:-([A-Za-z0-9]{3})-([A-Za-z0-9]{3})-((?:-?[0-9]{6})+)\+([0-9]{4})-([0-9]{7})-(.{8})-)?.*?$"
#prog = re.compile(reg, re.MULTILINE)
#groups = prog.match(sameData).groups()
while True:
try:
# Handle piped input
line=input().strip()
inp=input().strip()
except EOFError:
break
# only want EAS lines
if line.startswith("EAS:") or line.startswith("EAS (part):"):
content=line.split(maxsplit=1)[1]
if content=="NNNN": # end of EAS message
# write if we have something
if buff:
print("writing")
with open("alert.txt","w") as fh:
fh.write('\n'.join(buff))
# prepare for new data
buff.clear()
seen.clear()
elif content in seen:
# don't need repeats'
continue
else:
# check for national weather service
match=pattern.search(content)
if match:
seen.add(content)
msg=EAS2Text(content).EASText
print("got message", msg)
buff.append(msg)
# potentially take multiple lines in one buffered input
for line in inp.splitlines():
# only want EAS lines
if line.startswith("EAS:") or line.startswith("EAS (part):"):
content=line.split(":", maxsplit=1)[1].strip()
if content=="NNNN": # end of EAS message
# write if we have something
if buff:
print("writing")
with open("alert.txt","w") as fh:
fh.write('\n'.join(buff))
# prepare for new data
buff.clear()
seen.clear()
elif content in seen:
# don't need repeats
continue
else:
# check for national weather service
match=pattern.search(content)
if match:
seen.add(content)
msg=EAS2Text(content).EASText
print("got message", msg)
buff.append(msg)

View File

@@ -57,7 +57,7 @@ def parse_log_file(file_path):
if multiLogReader:
# set file_path to the cwd of the default project ../log
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
print(f"Checking log files: {log_files}")
if log_files:
@@ -745,7 +745,7 @@ def generate_main_html(log_data, system_info):
"""
template = Template(html_template)
return template.safe_substitute(
date=datetime.now().strftime('%Y_%m_%d'),
date=datetime.now().strftime('%Y-%m-%d'),
command_data=json.dumps(log_data['command_counts']),
message_data=json.dumps(log_data['message_types']),
activity_data=json.dumps(log_data['hourly_activity']),

View File

@@ -58,7 +58,7 @@ def parse_log_file(file_path):
if multiLogReader:
# set file_path to the cwd of the default project ../log
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
print(f"Checking log files: {log_files}")
if log_files:
@@ -1036,7 +1036,7 @@ options: {
template = Template(html_template)
return template.safe_substitute(
date=datetime.now().strftime('%Y_%m_%d'),
date=datetime.now().strftime('%Y-%m-%d'),
command_data=json.dumps(log_data['command_counts']),
message_data=json.dumps(log_data['message_types']),
activity_data=json.dumps(log_data['hourly_activity']),

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# # Simulate meshing-around de K7MHI 2024
from modules.log import * # Import the logger
from modules.log import * # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
import time
import random

View File

@@ -1,91 +1,175 @@
#!/bin/bash
# meshing-around install helper script
# install.sh
cd "$(dirname "$0")"
program_path=$(pwd)
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
printf "\n########################"
printf "\nMeshing Around Installer\n"
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
printf "\nChecking for dependencies\n"
printf "########################\n"
printf "\nThis script will try and install the Meshing Around Bot and its dependencies.\n"
printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded systems.\n"
printf "If there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# check if we are in /opt/meshing-around
if [ $program_path != "/opt/meshing-around" ]; then
printf "\nIt is suggested to project path to /opt/meshing-around\n"
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
read move
if [[ $(echo "$move" | grep -i "^y") ]]; then
sudo mv $program_path /opt/meshing-around
cd /opt/meshing-around
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
exit 0
fi
fi
# check write access to program path
if [[ ! -w ${program_path} ]]; then
printf "\nInstall path not writable, try running the installer with sudo\n"
exit 1
fi
# if hostname = femtofox, then we are on embedded
if [[ $(hostname) == "femtofox" ]]; then
printf "\nDetected femtofox embedded system\n"
embedded="y"
else
# check if running on embedded
printf "\nAre You installing into an embedded system like a luckfox or -native? most should say no here (y/n)"
read embedded
fi
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
printf "\nDetected embedded skipping dependency installation\n"
else
# Check and install dependencies
if ! command -v python3 &> /dev/null
then
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
sudo apt-get install python3 python3-pip
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, trying 'apt-get install python3-pip'\n"
sudo apt-get install python3-pip
fi
# double check for python3 and pip
if ! command -v python3 &> /dev/null
then
printf "python3 not found, please install python3 with your OS\n"
exit 1
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
exit 1
fi
printf "\nDependencies installed\n"
fi
# add user to groups for serial access
printf "\nAdding user to dialout and tty groups for serial access\n"
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
sudo usermod -a -G bluetooth $USER
# check for pip
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
sudo apt-get install python3-pip
else
printf "python pip found\n"
fi
# copy service files
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
# generate config file, check if it exists
if [ -f config.ini ]; then
if [[ -f config.ini ]]; then
printf "\nConfig file already exists, moving to backup config.old\n"
mv config.ini config.old
fi
cp config.template config.ini
printf "\nConfig file generated\n"
printf "\nConfig files generated!\n"
# update lat,long in config.ini
latlong=$(curl --silent --max-time 20 https://ipinfo.io/loc || echo "48.50,-123.0")
IFS=',' read -r lat lon <<< "$latlong"
sed -i "s|lat = 48.50|lat = $lat|g" config.ini
sed -i "s|lon = -123.0|lon = $lon|g" config.ini
echo "lat,long updated in config.ini to $latlong"
# set virtual environment and install dependencies
printf "\nMeshing Around Installer\n"
echo "Do you want to install the bot in a virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
exit 1
else
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
#check if python3 has venv module
if [ -f venv/bin/activate ]; then
printf "\nFpund virtual environment for python\n"
else
sudo apt-get install python3-venv
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
exit 1
fi
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies
pip install -U -r requirements.txt
fi
# check if running on embedded
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
printf "\nDetected embedded skipping venv\n"
else
printf "\nSkipping virtual environment...\n"
# install dependencies
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [ $rpi == "y" ]; then
pip install -U -r requirements.txt --break-system-packages
printf "\nRecomended install is in a python virtual environment, do you want to use venv? (y/n)"
read venv
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3/venv error, please install python3-venv with your OS\n"
exit 1
else
echo "The Following could be messy, or take some time on slower devices."
echo "Creating virtual environment..."
#check if python3 has venv module
if [[ -f venv/bin/activate ]]; then
printf "\nFound virtual environment for python\n"
python3 -m venv venv
source venv/bin/activate
else
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
sudo apt-get install python3-venv
fi
# create virtual environment
python3 -m venv venv
# double check for python3-venv
if [[ -f venv/bin/activate ]]; then
printf "\nFound virtual environment for python\n"
source venv/bin/activate
else
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
exit 1
fi
printf "\nVirtual environment created\n"
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies to venv
pip install -U -r requirements.txt
fi
else
pip install -U -r requirements.txt
printf "\nSkipping virtual environment...\n"
# install dependencies to system
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [[ $(echo "${rpi}" | grep -i "^y") ]]; then
pip install -U -r requirements.txt --break-system-packages
else
pip install -U -r requirements.txt
fi
fi
fi
printf "\n\n"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
read bot
# if $1 is passed
if [[ $1 == "mesh" ]]; then
bot="mesh"
elif [[ $1 == "pong" ]]; then
bot="pong"
else
printf "\n\n"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
echo "Pong bot is a simple bot for network testing"
echo "Mesh bot is a more complex bot more suited for meshing around"
echo "None will skip the service install"
read bot
fi
# set the correct path in the service file
replace="s|/dir/|$program_path/|g"
@@ -93,7 +177,32 @@ sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
# set the correct user in the service file?
whoami=$(whoami)
#ask if we should add a user for the bot
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
printf "\nDo you want to add a local user (meshbot) no login, for the bot? (y/n)"
read meshbotservice
fi
if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
sudo useradd -M meshbot
sudo usermod -L meshbot
sudo groupadd meshbot
sudo usermod -a -G meshbot meshbot
whoami="meshbot"
echo "Added user meshbot with no home directory"
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 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
@@ -102,58 +211,130 @@ replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sudo systemctl daemon-reload
printf "\n service files updated\n"
# ask if emoji font should be installed for linux
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [ $emoji == "y" ]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
if [ $bot == "pong" ]; then
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
# install service for pong bot
sudo cp etc/pong_bot.service /etc/systemd/system/
sudo systemctl enable pong_bot.service
sudo systemctl daemon-reload
echo "to start pong bot service: systemctl start pong_bot"
service="pong_bot"
fi
if [ $bot == "mesh" ]; then
if [[ $(echo "${bot}" | grep -i "^m") ]]; then
# install service for mesh bot
sudo cp etc/mesh_bot.service /etc/systemd/system/
sudo systemctl enable mesh_bot.service
sudo systemctl daemon-reload
echo "to start mesh bot service: systemctl start mesh_bot"
service="mesh_bot"
fi
if [ $bot == "n" ]; then
if [ -f launch.sh ]; then
printf "\nTo run the bot, use the command: ./launch.sh\n"
./launch.sh
# check if running on embedded for final steps
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
# ask if emoji font should be installed for linux
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [[ $(echo "${emoji}" | grep -i "^y") ]]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
fi
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
printf "ollama pull gemma2:2b\n"
printf "Total download is multi GB, recomend pi5/8GB or better for this\n"
# ask if the user wants to install the LLM Ollama components
printf "\nDo you want to install the LLM Ollama components? (y/n)"
read ollama
if [[ $(echo "${ollama}" | grep -i "^y") ]]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if the user wants to install the LLM Ollama components
echo "Do you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [ $gemma == "y" ]; then
ollama pull gemma2:2b
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
ollama pull gemma2:2b
fi
fi
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
fi
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
fi
read -p "Press enter to complete the installation, these commands saved to install_notes.txt"
printf "\nGood time to reboot? (y/n)"
read reboot
if [[ $(echo "${reboot}" | grep -i "^y") ]]; then
sudo reboot
fi
else
# we are on embedded
# replace "type = serial" with "type = tcp" in config.ini
replace="s|type = serial|type = tcp|g"
sed -i "$replace" config.ini
# replace "# hostname = meshtastic.local" with "hostname = localhost" in config.ini
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
sed -i "$replace" config.ini
printf "\nConfig file updated for embedded\n"
# 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
printf "Reference following commands:\n\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
fi
echo "Good time to reboot? (y/n)"
read reboot
if [ $reboot == "y" ]; then
sudo reboot
fi
printf "\nInstallation complete!\n"
exit 0
# to uninstall the product run the following commands as needed
# sudo systemctl stop mesh_bot
# sudo systemctl disable mesh_bot
# sudo systemctl stop pong_bot
# sudo systemctl disable pong_bot
# sudo systemctl stop mesh_bot_reporting
# sudo systemctl disable mesh_bot_reporting
# sudo rm /etc/systemd/system/mesh_bot.service
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
# sudo rm /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl reset-failed
# sudo gpasswd -d meshbot dialout
# sudo gpasswd -d meshbot tty
# sudo gpasswd -d meshbot bluetooth
# sudo groupdel meshbot
# sudo userdel meshbot
# sudo rm -rf /opt/meshing-around
# after install shenannigans
# add 'bee = True' to config.ini General section. You will likley want to clean the txt up a bit
# wget https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt -O bee.txt

View File

@@ -1,4 +1,5 @@
#!/bin/bash
# This script launches the meshing-around bot or the report generator in python virtual environment
# launch.sh
cd "$(dirname "$0")"

View File

@@ -14,6 +14,8 @@ Logging messages to disk or 'Syslog' to disk uses the python native logging func
LogMessagesToFile = False
# Logging of system messages to file, needed for reporting engine
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
@@ -23,4 +25,7 @@ To change the stdout (what you see on the console) logging level (default is DEB
```
# Set level for stdout handler
stdout_handler.setLevel(logging.INFO)
```
```
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

View File

@@ -2,10 +2,15 @@
# Meshtastic Autoresponder MESH Bot
# K7MHI Kelly Keeton 2024
try:
from pubsub import pub
except ImportError:
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh mesh' using a virtual environment.")
exit(1)
import asyncio
import time # for sleep, get some when you can :)
import random
from pubsub import pub # pip install pubsub
from modules.log import *
from modules.system import *
@@ -14,8 +19,8 @@ restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golf
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
cmdHistory = [] # list to hold the last commands
DEBUGpacket = False # Debug print the packet rx
DEBUGhops = False # Debug print hop info and bad hop count packets
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
global cmdHistory
@@ -23,12 +28,12 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
message_lower = message.lower()
bot_response = "🤖I'm sorry, I'm afraid I can't do that."
# Command List
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
default_commands = {
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbsack": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"bbshelp": bbs_help,
"bbsinfo": lambda: get_bbs_stats(),
@@ -37,11 +42,15 @@ 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),
"clearsms": lambda: handle_sms(message_from_id, message),
"cmd": lambda: help_message,
"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),
"cmd": lambda: help_message,
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
"ealert": lambda: handle_emergency_alerts(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),
@@ -57,24 +66,42 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"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!!🛜",
"readnews": lambda: read_news(),
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
"setemail": lambda: handle_email(message_from_id, message),
"setsms": lambda: handle_sms( message_from_id, message),
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"sms:": lambda: handle_sms(message_from_id, message),
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"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),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"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'),
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
"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),
"🐝": lambda: read_file("bee.txt", True),
# any value from system.py:trap_list_emergency will trigger the emergency function
"112": lambda: handle_emergency(message_from_id, deviceID, message),
"911": lambda: handle_emergency(message_from_id, deviceID, message),
"999": lambda: handle_emergency(message_from_id, deviceID, message),
"ambulance": lambda: handle_emergency(message_from_id, deviceID, message),
"emergency": lambda: handle_emergency(message_from_id, deviceID, message),
"fire": lambda: handle_emergency(message_from_id, deviceID, message),
"police": lambda: handle_emergency(message_from_id, deviceID, message),
"rescue": lambda: handle_emergency(message_from_id, deviceID, message),
}
# set the command handler
@@ -112,6 +139,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
@@ -120,7 +148,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
if "ping" in message.lower():
msg = "🏓PONG\n"
type = "🏓PING\n"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
"🎙Testing, testing\n",\
@@ -131,10 +159,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
myname = get_name_from_number(myNodeNum, 'short', deviceID)
msg = f"QSP QSL OM DE {myname} K\n"
else:
msg = "🔊 Can you hear me now?"
@@ -151,6 +176,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
msg = msg + " #" + message.split("#")[1]
type = type + " #" + message.split("#")[1]
# check for multi ping request
if " " in message:
# if stop multi ping
@@ -159,18 +185,37 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
if multiPingList[i].get('message_from_id') == message_from_id:
multiPingList.pop(i)
msg = "🛑 auto-ping"
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number})
msg = f"🚦Initalizing {pingCount} auto-ping"
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
@@ -178,6 +223,33 @@ def handle_alertBell(message_from_id, deviceID, message):
msg = ["the only prescription is more 🐮🔔🐄🛎️", "what this 🤖 needs is more 🐮🔔🐄🛎️", "🎤ring my bell🛎🔔🎶"]
return random.choice(msg)
def handle_emergency(message_from_id, deviceID, message):
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
# if user in bbs_ban_list return
if str(message_from_id) in bbs_ban_list:
# silent discard
logger.warning(f"System: {message_from_id} on spam list, no emergency responder alert sent")
return ''
# trgger alert to emergency_responder_alert_channel
if message_from_id != 0:
nodeLocation = get_node_location(message_from_id, deviceID)
# if default location is returned set to Unknown
if nodeLocation[0] == latitudeValue and nodeLocation[1] == longitudeValue:
nodeLocation = ["?", "?"]
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(myNodeNum, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}"
# alert the emergency_responder_alert_channel
time.sleep(responseDelay)
send_message(msg, emergency_responder_alert_channel, 0, emergency_responder_alert_interface)
logger.warning(f"System: {message_from_id} Emergency Assistance Requested in {message}")
# send the message out via email/sms
if enableSMTP:
for user in sysopEmails:
send_email(user, f"Emergency Assistance Requested by {nodeInfo} in {message}", message_from_id)
# respond to the user
time.sleep(responseDelay + 2)
return EMERGENCY_RESPONSE
def handle_motd(message, message_from_id, isDM):
global MOTD
isAdmin = False
@@ -213,9 +285,9 @@ def handle_wxalert(message_from_id, deviceID, message):
location = get_node_location(message_from_id, deviceID)
if "wxalert" in message:
# Detailed weather alert
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
weatherAlert = getActiveWeatherAlertsDetailNOAA(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
weatherAlert = getWeatherAlertsNOAA(str(location[0]), str(location[1]))
if NO_ALERTS not in weatherAlert:
weatherAlert = weatherAlert[0]
@@ -237,6 +309,34 @@ llmRunCounter = 0
llmTotalRuntime = []
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
def handle_satpass(message_from_id, deviceID, channel_number, message):
location = get_node_location(message_from_id, deviceID)
passes = ''
satList = satListConfig
message = message.lower()
# if user has a NORAD ID in the message
if "satpass " in message:
try:
userList = message.split("satpass ")[1].split(" ")[0]
#split userList and make into satList overrided the config.ini satList
satList = userList.split(",")
except:
return "example use:🛰satpass 25544,33591"
# Detailed satellite pass
for bird in satList:
satPass = getNextSatellitePass(bird, str(location[0]), str(location[1]))
if satPass:
# append to passes
passes = passes + satPass + "\n"
# remove the last newline
passes = passes[:-1]
if passes == '':
passes = "No 🛰️ anytime soon"
return passes
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
location_name = 'no location provided'
@@ -395,8 +495,9 @@ def handleLemonade(message, nodeID, deviceID):
if highScore != 0:
if highScore['userID'] != 0:
nodeName = get_name_from_number(highScore['userID'])
if nodeName.isnumeric() and interface2_enabled:
nodeName = get_name_from_number(highScore['userID'], 'long', 2)
if nodeName.isnumeric() and multiple_interface:
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
#nodeName = get_name_from_number(highScore['userID'], 'long', 2)
msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k "
msg += start_lemonade(nodeID=nodeID, message=message, celsius=False)
@@ -435,8 +536,9 @@ def handleBlackJack(message, nodeID, deviceID):
if highScore != 0:
if highScore['nodeID'] != 0:
nodeName = get_name_from_number(highScore['nodeID'])
if nodeName.isnumeric() and interface2_enabled:
nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
if nodeName.isnumeric() and multiple_interface:
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
#nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} chips. "
time.sleep(responseDelay + 1) # short answers with long replies can cause message collision added wait
return msg
@@ -470,8 +572,9 @@ def handleVideoPoker(message, nodeID, deviceID):
if highScore != 0:
if highScore['nodeID'] != 0:
nodeName = get_name_from_number(highScore['nodeID'])
if nodeName.isnumeric() and interface2_enabled:
nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
if nodeName.isnumeric() and multiple_interface:
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
#nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} coins. "
if last_cmd != "" and nodeID != 0:
@@ -549,22 +652,58 @@ def handleGolf(message, nodeID, deviceID):
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
else:
userRiver = userRiver.split(",") if "," in userRiver else 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)
return msg
def handle_wxc(message_from_id, deviceID, cmd):
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
#logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
weather = get_wx_meteo(str(location[0]), str(location[1]))
elif use_meteo_wxApi:
logger.debug("System: Bot Returning Open-Meteo API for weather metric")
#logger.debug("System: Bot Returning Open-Meteo API for weather metric")
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
logger.debug("System: Bot Returning NOAA API for weather metric")
weather = get_weather(str(location[0]), str(location[1]), 1)
#logger.debug("System: Bot Returning NOAA API for weather metric")
weather = get_NOAAweather(str(location[0]), str(location[1]), 1)
else:
logger.debug("System: Bot Returning NOAA API for weather imperial")
weather = get_weather(str(location[0]), str(location[1]))
#logger.debug("System: Bot Returning NOAA API for weather imperial")
weather = get_NOAAweather(str(location[0]), str(location[1]))
return weather
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 message.lower().startswith("ealert"):
# Detailed alert FEMA
return getIpawsAlert(str(location[0]), str(location[1]))
else:
# Headlines only FEMA
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
def handle_bbspost(message, message_from_id, deviceID):
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
@@ -631,6 +770,16 @@ def handle_sun(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_sun(str(location[0]), str(location[1]))
def sysinfo(message, message_from_id, deviceID):
if "?" in message:
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
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
@@ -646,6 +795,9 @@ def handle_lheard(message, nodeid, deviceID, isDM):
else:
# trim the last \n
bot_response = bot_response[:-1]
# get count of nodes heard
bot_response += f"\n👀In Mesh: {len(seenNodes)}"
# bot_response += getNodeTelemetry(deviceID)
return bot_response
@@ -718,7 +870,7 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
def handle_tide(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_tide(str(location[0]), str(location[1]))
return get_NOAAtide(str(location[0]), str(location[1]))
def handle_moon(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
@@ -749,6 +901,46 @@ def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
msg = "Error in whoami"
return msg
def handle_whois(message, deviceID, channel_number, message_from_id):
#return data on a node name or number
if "?" in message:
return message.split("?")[0].title() + " command returns information on a node."
else:
# get the nodeID from the message
msg = ''
node = ''
# find the requested node in db
if " " in message:
node = message.split(" ")[1]
if node.startswith("!") and len(node) == 9:
# mesh !hex
try:
node = int(node.strip("!"),16)
except ValueError as e:
node = 0
elif node.isalpha() or not node.isnumeric():
# try short name
node = get_num_from_short_name(node, deviceID)
# get details on the node
for i in range(len(seenNodes)):
if seenNodes[i]['nodeID'] == int(node):
msg = f"Node: {seenNodes[i]['nodeID']} is {get_name_from_number(seenNodes[i]['nodeID'], 'long', deviceID)}\n"
msg += f"Last 👀: {time.ctime(seenNodes[i]['lastSeen'])} "
break
if msg == '':
msg = "Provide a valid node number or short name"
else:
# if the user is an admin show the channel and interface and location
if str(message_from_id) in bbs_admin_list:
location = get_node_location(seenNodes[i]['nodeID'], deviceID, channel_number)
msg += f"Ch: {seenNodes[i]['channel']}, Int: {seenNodes[i]['rxInterface']}"
msg += f"Lat: {location[0]}, Lon: {location[1]}\n"
if location != [latitudeValue, longitudeValue]:
msg += f"Loc: {where_am_i(str(location[0]), str(location[1]))}"
return msg
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
global llm_enabled
@@ -789,6 +981,7 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
return playingGame
def onReceive(packet, interface):
global seenNodes
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
@@ -798,6 +991,8 @@ def onReceive(packet, interface):
# Valies assinged to the packet
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
pkiStatus = (False, 'ABC')
replyIDset = False
emojiSeen = False
isDM = False
if DEBUGpacket:
@@ -807,35 +1002,55 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface:
rxNode = 1
elif interface2_enabled and port2 in rxInterface:
rxNode = 2
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp':
rxNode = 1
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# set the message_from_id
message_from_id = packet['from']
# if message_from_id is not in the seenNodes list add it
if not any(node['nodeID'] == message_from_id for node in seenNodes):
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'lastSeen': time.time()})
# BBS DM MAIL CHECKER
if bbs_enabled and 'decoded' in packet:
message_from_id = packet['from']
msg = bbs_check_dm(message_from_id)
if msg:
@@ -851,7 +1066,10 @@ 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')
message_from_id = packet['from']
# 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")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
@@ -860,7 +1078,15 @@ def onReceive(packet, interface):
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has replyId flag // currently unused in the code
if packet.get('replyId'):
replyIDset = packet.get('replyId', False)
# check if the packet has emoji flag set it // currently unused in the code
if packet.get('emoji'):
emojiSeen = packet.get('emoji', False)
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
@@ -876,9 +1102,18 @@ def onReceive(packet, interface):
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
if DEBUGhops:
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_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0:
hop = "MQTT"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
@@ -889,13 +1124,13 @@ def onReceive(packet, interface):
hop = f"{hop_count} hops"
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
@@ -910,22 +1145,42 @@ def onReceive(packet, interface):
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
else:
playingGame = False
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
if games_enabled:
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
playingGame = False
if not playingGame:
if llm_enabled:
# respond with LLM
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
# if seenNodes list is not marked as welcomed send welcome message
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
# send welcome message
send_message(welcome_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# mark the node as welcomed
for node in seenNodes:
if node['nodeID'] == message_from_id:
node['welcome'] = True
else:
if dad_jokes_enabled:
# respond with a dad joke on DM
send_message(tell_joke(), channel_number, message_from_id, rxNode)
else:
# respond with help message on DM
send_message(help_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
@@ -973,45 +1228,53 @@ def onReceive(packet, interface):
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
if repeater_enabled and multiple_interface:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
send_message(rMsg, channel_number, 0, i)
time.sleep(responseDelay)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
consumeMetadata(packet, rxNode)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ", myNodeNum1)
logger.debug(f"System: LLM model {llmModel} loaded")
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False):
myNodeNum = globals().get(f'myNodeNum{i}', 0)
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ")
logger.debug(f"System: LLM model {llmModel} loaded")
if log_messages_to_file:
logger.debug("System: Logging Messages to disk")
if syslog_to_file:
logger.debug("System: Logging System Logs to disk")
if bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if bbs_link_enabled:
if len(bbs_link_whitelist) > 0:
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
else:
logger.debug(f"System: BBS Link Enabled allowing all")
if solar_conditions_enabled:
logger.debug("System: Celestial Telemetry Enabled")
if location_enabled:
@@ -1033,17 +1296,35 @@ async def start_rx():
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_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}")
if enable_runShellCmd:
logger.debug(f"System: Shell Command monitor enabled")
if read_news_enabled:
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
if bee_enabled:
logger.debug(f"System: File Monitor Bee Monitor Enabled for bee.txt")
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}")
if emergency_responder_enabled:
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
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"))
# 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))
@@ -1060,16 +1341,17 @@ async def start_rx():
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send a joke every 2 minutes using tell_joke function to channel 2 on device 1
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 0 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 0, 0, 1))
#
# 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()
@@ -1088,8 +1370,10 @@ async def main():
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.gather(meshRxTask, watchdogTask)
await asyncio.gather(hamlibTask)
await asyncio.gather(fileMonTask)
if radio_detection_enabled:
await asyncio.gather(hamlibTask)
if file_monitor_enabled:
await asyncio.gather(fileMonTask)
await asyncio.sleep(0.01)

View File

@@ -3,6 +3,7 @@
import pickle # pip install pickle
from modules.log import *
import time
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
@@ -164,6 +165,15 @@ def bbs_delete_dm(toNode, message):
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# check if the bbs link is enabled
if bbs_link_whitelist is not None:
if str(peerNode) not in bbs_link_whitelist:
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
return "System: BBS Link is disabled for your node."
if bbs_link_enabled == False:
return "System: BBS Link is disabled."
# respond when another bot asks for the bbs posts to sync
if "bbslink" in input.lower():
if "$" in input and "#" in input:
@@ -178,8 +188,12 @@ def bbs_sync_posts(input, peerNode, RxNode):
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
# send message
# send message with delay to keep chutil happy
if messageID < len(bbs_messages):
time.sleep(5 + responseDelay)
# every 5 messages add extra delay
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))

View File

@@ -3,17 +3,49 @@
from modules.log import *
import asyncio
import random
import os
async def watch_file():
def read_file(file_monitor_file_path):
try:
trap_list_filemon = ("readnews",)
def read_file(file_monitor_file_path, random_line_only=False):
try:
if not os.path.exists(file_monitor_file_path):
logger.warning(f"FileMon: File not found: {file_monitor_file_path}")
if file_monitor_file_path == "bee.txt":
return "🐝buzz 💐buzz buzz🍯"
if random_line_only:
# read a random line from the file
with open(file_monitor_file_path, 'r') as f:
lines = f.readlines()
return random.choice(lines)
else:
# read the whole file
with open(file_monitor_file_path, 'r') as f:
content = f.read()
return content
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
def read_news():
# read the news file on demand
return read_file(news_file_path, read_news_enabled)
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:
f.write(content)
logger.info(f"FileMon: Updated {news_file_path}")
return True
except Exception as e:
logger.warning(f"FileMon: Error writing file: {news_file_path}")
return False
async def watch_file():
if not os.path.exists(file_monitor_file_path):
return None
@@ -29,4 +61,24 @@ async def watch_file():
content = content.replace('\n', ' ').replace('\r', '').strip()
if content:
return content
await asyncio.sleep(1) # Check every
await asyncio.sleep(1) # Check every
def call_external_script(message, script="script/runShell.sh"):
try:
# Debugging: Print the current working directory and resolved script path
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
if not os.path.exists(script_path):
# try the raw script name
script_path = script
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
output = os.popen(f"bash {script_path} {message}").read()
return output
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
return None

View File

@@ -159,7 +159,7 @@ def get_found_items(nodeID):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
dwInventoryDb[i]['amount'] = amount
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
msg = f"💊You found {qty} {my_drugs[found].name}"
else:
# rolls to see how much cash the user finds
cash_found = random.randint(1, 977)
@@ -241,6 +241,8 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
buy_amount = cash // price_list[drug_choice]
if buy_amount > 100 - inventory:
buy_amount = 100 - inventory
if buy_amount == 0:
return "You don\'t have any empty inventory slots.🎒"
# set the buy amount to the max if the user enters m
buy_amount = int(buy_amount)

View File

@@ -117,6 +117,10 @@ def sendWithEmoji(message):
def tell_joke(nodeID=0):
dadjoke = Dadjoke()
renderedLaugh = sendWithEmoji(dadjoke.joke)
if dad_jokes_emojiJokes:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke
return renderedLaugh

60
modules/globalalert.py Normal file
View File

@@ -0,0 +1,60 @@
# helper functions to use location data for data outside US/north america
# K7MHI Kelly Keeton 2024
import json # pip install json
from geopy.geocoders import Nominatim # pip install geopy
import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
def get_govUK_alerts(shortAlerts=False):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'
response = requests.get(url)
soup = bs.BeautifulSoup(response.text, 'html.parser')
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
except Exception as e:
logger.warning("Error getting UK alerts: " + str(e))
return NO_ALERTS
if alert:
return "🚨" + alert.get_text(strip=True)
else:
return NO_ALERTS
def get_wxUKgov():
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
try:
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
response = requests.get(url)
soup = bs.BeautifulSoup(response.content, 'xml')
items = soup.find_all('item')
alerts = []
for item in items:
title = item.find('title').get_text(strip=True)
description = item.find('description').get_text(strip=True)
alerts.append(f"🚨 {title}: {description}")
return "\n".join(alerts) if alerts else NO_ALERTS
except Exception as e:
logger.warning("Error getting UK weather warnings: " + str(e))
return NO_ALERTS
def get_floodUKgov():
# get UK flood warnings
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
return NO_ALERTS

73
modules/gpio.py Normal file
View File

@@ -0,0 +1,73 @@
# GPIO module for MeshLink, concept code, not implemented
# K7MHI Kelly Keeton 2024
# https://pypi.org/project/gpio/
#import gpio
# https://pythonhosted.org/RPIO/
import RPIO
from modules.log import *
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
# set up input channel without pull-up
RPIO.setup(7, RPIO.IN)
# set up input channel with pull-up
RPIO.setup(8, RPIO.IN, pull_up_down=RPIO.PUD_UP)
# set up GPIO output channel
RPIO.setup(8, RPIO.OUT)
# change to BOARD numbering schema
RPIO.setmode(RPIO.BOARD)
# set up PWM channel
RPIO.setup(12, RPIO.OUT)
p = RPIO.PWM(12)
def gpio_status():
# get status of GPIO pins
gpio_status = ""
gpio_status += "GPIO 7: " + str(RPIO.input(7)) + "\n"
gpio_status += "GPIO 8: " + str(RPIO.input(8)) + "\n"
gpio_status += "GPIO 12: " + str(RPIO.input(12)) + "\n"
return gpio_status
def gpio_toggle():
# toggle GPIO pin 8
RPIO.output(8, not RPIO.input(8))
return "GPIO 8 toggled"
def gpio_pwm():
# set PWM on GPIO pin 12
p.start(50)
return "PWM started"
def gpio_stop():
# stop PWM on GPIO pin 12
p.stop()
return "PWM stopped"
def gpio_shutdown():
# shutdown GPIO
RPIO.cleanup()
return "GPIO shutdown"
def trap_gpio(message):
# trap for GPIO commands
if "status" in message:
return gpio_status()
elif "toggle" in message:
return gpio_toggle()
elif "pwm" in message:
return gpio_pwm()
elif "stop" in message:
return gpio_stop()
elif "shutdown" in message:
return gpio_shutdown()
else:
return "GPIO command not recognized"

View File

@@ -1,27 +1,32 @@
#!/usr/bin/env python3
# LLM Module for meshing-around
# This module is used to interact with Ollama to generate responses to user input
# This module is used to interact with LLM API to generate responses to user input
# K7MHI Kelly Keeton 2024
from modules.log import *
# Ollama Client
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
from ollama import Client as OllamaClient
import requests
import json
from googlesearch import search # pip install googlesearch-python
# This is my attempt at a simple RAG implementation it will require some setup
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
# This is lighter weight and can be used in a standalone environment, needs chromadb
# "chat with a file" is the use concept here, the file is the RAG data
# is anyone using this please let me know if you are Dec62024 -kelly
ragDEV = False
if ragDEV:
import os
import ollama # pip install ollama
import chromadb # pip install chromadb
from ollama import Client as OllamaClient
ollamaClient = OllamaClient(host=ollamaHostName)
# LLM System Variables
ollamaClient = OllamaClient(host=ollamaHostName)
ollamaAPI = ollamaHostName + "/api/generate"
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
llmEnableHistory = True # enable last message history for the LLM model
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
@@ -193,20 +198,26 @@ def llm_query(input, nodeID=0, location_name=None):
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
# Query the model with RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
if isinstance(result, dict):
result = result.get("response")
else:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# Query the model without RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
if isinstance(result, dict):
result = result.get("response")
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False}
# Query the model via Ollama web API
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
# Condense the result to just needed
if result.status_code == 200:
result_json = result.json()
result = result_json.get("response", "")
else:
raise Exception(f"HTTP Error: {result.status_code}")
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
logger.warning(f"System: LLM failure: {e}")
return "I am having trouble processing your request, please try again later."
return "⛔️I am having trouble processing your request, please try again later."
# cleanup for message output
response = result.strip().replace('\n', ' ')
@@ -217,15 +228,3 @@ def llm_query(input, nodeID=0, location_name=None):
llmChat_history[nodeID] = [input, response]
return response
# import subprocess
# def get_ollama_cpu():
# try:
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
# if "GPU" in psOutput.stdout:
# logger.debug(f"System: Ollama process with GPU")
# else:
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False

View File

@@ -1,4 +1,4 @@
# helper functions to use location data like NOAA weather
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
# K7MHI Kelly Keeton 2024
import json # pip install json
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
@@ -154,9 +154,8 @@ def getArtSciRepeaters(lat=0, lon=0):
else:
msg = f"no results.. sorry"
return msg
def get_tide(lat=0, lon=0):
def get_NOAAtide(lat=0, lon=0):
station_id = ""
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, try sending location for tide")
@@ -172,7 +171,7 @@ def get_tide(lat=0, lon=0):
if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA
return "No tide station found with info provided"
station_id = station_json['stationList'][0]['stationId']
@@ -219,7 +218,7 @@ def get_tide(lat=0, lon=0):
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
def get_NOAAweather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
weather = ""
if float(lat) == 0 and float(lon) == 0:
@@ -255,7 +254,7 @@ def get_weather(lat=0, lon=0, unit=0):
# extract data from rows
for row in rows:
# shrink the text
line = abbreviate_weather(row.text)
line = abbreviate_noaa(row.text)
# only grab a few days of weather
if len(weather.split("\n")) < forecastDuration:
weather += line + "\n"
@@ -263,7 +262,7 @@ def get_weather(lat=0, lon=0, unit=0):
weather = weather[:-1]
# get any alerts and return the count
alerts = getWeatherAlerts(lat, lon)
alerts = getWeatherAlertsNOAA(lat, lon)
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
alert = ""
@@ -278,23 +277,23 @@ def get_weather(lat=0, lon=0, unit=0):
return weather
def abbreviate_weather(row):
def abbreviate_noaa(row):
# replace long strings with shorter ones for display
replacements = {
"Monday": "Mon ",
"Tuesday": "Tue ",
"Wednesday": "Wed ",
"Thursday": "Thu ",
"Friday": "Fri ",
"Saturday": "Sat ",
"Sunday": "Sunday ",
"Today": "Today ",
"Night": "Night ",
"Tonight": "Tonight ",
"Tomorrow": "Tomorrow ",
"Day": "Day ",
"This Afternoon": "Afternoon ",
"Overnight": "Overnight ",
"monday": "Mon ",
"tuesday": "Tue ",
"wednesday": "Wed ",
"thursday": "Thu ",
"friday": "Fri ",
"saturday": "Sat ",
"sunday": "Sun ",
"today": "Today ",
"night": "Night ",
"tonight": "Tonight ",
"tomorrow": "Tomorrow ",
"day": "Day ",
"this afternoon": "Afternoon ",
"overnight": "Overnight ",
"northwest": "NW",
"northeast": "NE",
"southwest": "SW",
@@ -303,29 +302,37 @@ def abbreviate_weather(row):
"south": "S",
"east": "E",
"west": "W",
"Northwest": "NW",
"Northeast": "NE",
"Southwest": "SW",
"Southeast": "SE",
"North": "N",
"South": "S",
"East": "E",
"West": "W",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms",
"thunderstorm": "t-storm",
"quarters": "qtrs",
"quarter": "qtr"
"quarter": "qtr",
"january": "Jan",
"february": "Feb",
"march": "Mar",
"april": "Apr",
"may": "May",
"june": "Jun",
"july": "Jul",
"august": "Aug",
"september": "Sep",
"october": "Oct",
"november": "Nov",
"december": "Dec",
"degrees": "°",
"percent": "%",
"department": "Dept.",
}
line = row
for key, value in replacements.items():
line = line.replace(key, value)
# case insensitive replace
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
return line
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
@@ -350,11 +357,13 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
alerts = ""
alertxml = xml.dom.minidom.parseString(alert_data.text)
for i in alertxml.getElementsByTagName("entry"):
alerts += (
i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n"
)
title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue
area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue
if enableExtraLocationWx:
alerts += f"{title}. {area_desc.replace(' ', '')}\n"
else:
alerts += f"{title}\n"
if alerts == "" or alerts == None:
return NO_ALERTS
@@ -367,30 +376,33 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
alert_num = 0
alert_num = len(alerts.split("\n"))
alerts = abbreviate_weather(alerts)
alerts = abbreviate_noaa(alerts)
# return the first ALERT_COUNT alerts
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
return data
wxAlertCache = ""
def alertBrodcast():
wxAlertCacheNOAA = ""
def alertBrodcastNOAA():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
if currentAlert[0] == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
wxAlertCache = ""
global wxAlertCacheNOAA
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
# check if any reason to discard the alerts
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
return False
elif currentAlert == NO_ALERTS:
wxAlertCacheNOAA = ""
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
elif currentAlert[0] not in wxAlertCacheNOAA:
# Check if the current alert is not in the weather alert cache
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
wxAlertCacheNOAA = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
if float(lat) == 0 and float(lon) == 0:
@@ -425,7 +437,7 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
"\n***\n"
)
alerts = abbreviate_weather(alerts)
alerts = abbreviate_noaa(alerts)
# trim the alerts to the first ALERT_COUNT
alerts = alerts.split("\n***\n")[:numWxAlerts]
@@ -440,3 +452,163 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
alerts = "\n".join(alerts)
return alerts
def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# get the latest IPAWS alert from FEMA
alert = ''
alerts = []
# set the API URL for IPAWS
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
if ipawsPIN != "000000":
alert_url += "?pin=" + ipawsPIN
# get the alerts from FEMA
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
return ERROR_FETCHING_DATA
# main feed bulletins
alertxml = xml.dom.minidom.parseString(alert_data.text)
# extract alerts from main feed
for entry in alertxml.getElementsByTagName("entry"):
link = entry.getElementsByTagName("link")[0].getAttribute("href")
try:
#pin check
if ipawsPIN != "000000":
link += "?pin=" + ipawsPIN
# get the linked alert data from FEMA
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
if not linked_data.ok:
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
continue
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)
for info in linked_xml.getElementsByTagName("info"):
# extract values from XML
sameVal = "NONE"
geocode_value = "NONE"
description = ""
try:
eventCode_table = info.getElementsByTagName("eventCode")[0]
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
# use headline if no description
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
else:
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):
# ignore the FEMA test alerts
if ignoreFEMAtest:
if "Test" in headline:
logger.debug(f"System: Ignoring FEMA Test Alert: {headline} for {areaDesc}")
continue
# add to alerts list
alerts.append({
'alertType': alertType,
'alertCode': alertCode,
'headline': headline,
'areaDesc': areaDesc,
'geocode_type': geocode_type,
'geocode_value': geocode_value,
'description': description
})
# else:
# # these are discarded some day but logged for debugging currently
# logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
# return the numWxAlerts of alerts
if len(alerts) > 0:
for alertItem in alerts[:numWxAlerts]:
if shortAlerts:
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}")
else:
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}\n{alertItem['description']}")
# add a newline if not the last alert
if alertItem != alerts[:numWxAlerts][-1]:
alert += "\n"
else:
alert = NO_ALERTS
return alert
def get_flood_noaa(lat=0, lon=0, uid=0):
# get the latest flood alert from NOAA
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
headers = {'accept': 'application/json'}
if uid == 0:
return "No flood gauge data found"
try:
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
if not response.ok:
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
data = response.json()
if not data:
return "No flood gauge data found"
# extract values from JSON
try:
name = data['name']
status_observed_primary = data['status']['observed']['primary']
status_observed_primary_unit = data['status']['observed']['primaryUnit']
status_observed_secondary = data['status']['observed']['secondary']
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
status_observed_floodCategory = data['status']['observed']['floodCategory']
status_forecast_primary = data['status']['forecast']['primary']
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
status_forecast_secondary = data['status']['forecast']['secondary']
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
# except KeyError as e:
# print(f"Missing key in data: {e}")
# except TypeError as e:
# print(f"Type error in data: {e}")
except Exception as e:
logger.debug("Location:Error extracting flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
# format the flood data
logger.debug(f"System: NOAA Flood data for {str(uid)}")
flood_data = f"Flood Data {name}:\n"
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
return flood_data

View File

@@ -3,6 +3,11 @@ from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from modules.settings import *
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
if not LOGGING_LEVEL:
LOGGING_LEVEL = "DEBUG"
LOGGING_LEVEL = getattr(logging, LOGGING_LEVEL)
class CustomFormatter(logging.Formatter):
grey = '\x1b[38;21m'
@@ -41,7 +46,7 @@ class plainFormatter(logging.Formatter):
# Create logger
logger = logging.getLogger("MeshBot System Logger")
logger.setLevel(logging.DEBUG)
logger.setLevel(LOGGING_LEVEL)
logger.propagate = False
msgLogger = logging.getLogger("MeshBot Messages Logger")
@@ -56,7 +61,7 @@ today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
# Set level for stdout handler (logs DEBUG level and above)
stdout_handler.setLevel(logging.DEBUG)
stdout_handler.setLevel(LOGGING_LEVEL)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Add handlers to the logger
@@ -65,7 +70,7 @@ logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
file_handler_sys.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
@@ -74,4 +79,22 @@ if log_messages_to_file:
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)
msgLogger.addHandler(file_handler)
# Pretty Timestamp
def getPrettyTime(seconds):
# convert unix time to minutes, hours, or days, or years for simple display
designator = "s"
if seconds > 0:
seconds = round(seconds / 60)
designator = "m"
if seconds > 60:
seconds = round(seconds / 60)
designator = "h"
if seconds > 24:
seconds = round(seconds / 24)
designator = "d"
if seconds > 365:
seconds = round(seconds / 365)
designator = "y"
return str(seconds) + designator

View File

@@ -5,9 +5,10 @@ import configparser
# messages
NO_DATA_NOGPS = "No location data: does your device have GPS?"
ERROR_FETCHING_DATA = "error fetching data"
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd? for more'
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, CMD? for more'
EMERGENCY_RESPONSE = "MeshBot detected a possible request for Emergency Assistance and alerted a wider audience."
MOTD = 'Thanks for using MeshBOT! Have a good day!'
NO_ALERTS = "No weather alerts found."
NO_ALERTS = "No alerts found."
# setup the global variables
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
@@ -24,10 +25,11 @@ max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
retry_int1 = False
retry_int2 = False
scheduler_enabled = False # enable the scheduler currently config via code only
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -41,48 +43,60 @@ except Exception as e:
if config.sections() == []:
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD,
'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config.write(open(config_file, 'w'))
print (f"System: Config file created, check {config_file} or review the config.template")
if 'sentry' not in config:
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
if 'repeater' not in config:
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
if 'radioMon' not in config:
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
if 'games' not in config:
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
if 'messagingSettings' not in config:
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
if 'fileMon' not in config:
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
if 'scheduler' not in config:
config['scheduler'] = {'enabled': 'False'}
config.write(open(config_file, 'w'))
if 'emergencyHandler' not in config:
config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''}
config.write(open(config_file, 'w'))
if 'smtp' not in config:
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
hostname1 = config['interface'].get('hostname', '')
mac1 = config['interface'].get('mac', '')
interface1_enabled = True # gotta have at least one interface
# interface2 settings
if 'interface2' in config:
@@ -94,7 +108,82 @@ if 'interface2' in config:
else:
interface2_enabled = False
# variables
# interface3 settings
if 'interface3' in config:
interface3_type = config['interface3'].get('type', 'serial')
port3 = config['interface3'].get('port', '')
hostname3 = config['interface3'].get('hostname', '')
mac3 = config['interface3'].get('mac', '')
interface3_enabled = config['interface3'].getboolean('enabled', False)
else:
interface3_enabled = False
# interface4 settings
if 'interface4' in config:
interface4_type = config['interface4'].get('type', 'serial')
port4 = config['interface4'].get('port', '')
hostname4 = config['interface4'].get('hostname', '')
mac4 = config['interface4'].get('mac', '')
interface4_enabled = config['interface4'].getboolean('enabled', False)
else:
interface4_enabled = False
# interface5 settings
if 'interface5' in config:
interface5_type = config['interface5'].get('type', 'serial')
port5 = config['interface5'].get('port', '')
hostname5 = config['interface5'].get('hostname', '')
mac5 = config['interface5'].get('mac', '')
interface5_enabled = config['interface5'].getboolean('enabled', False)
else:
interface5_enabled = False
# interface6 settings
if 'interface6' in config:
interface6_type = config['interface6'].get('type', 'serial')
port6 = config['interface6'].get('port', '')
hostname6 = config['interface6'].get('hostname', '')
mac6 = config['interface6'].get('mac', '')
interface6_enabled = config['interface6'].getboolean('enabled', False)
else:
interface6_enabled = False
# interface7 settings
if 'interface7' in config:
interface7_type = config['interface7'].get('type', 'serial')
port7 = config['interface7'].get('port', '')
hostname7 = config['interface7'].get('hostname', '')
mac7 = config['interface7'].get('mac', '')
interface7_enabled = config['interface7'].getboolean('enabled', False)
else:
interface7_enabled = False
# interface8 settings
if 'interface8' in config:
interface8_type = config['interface8'].get('type', 'serial')
port8 = config['interface8'].get('port', '')
hostname8 = config['interface8'].get('hostname', '')
mac8 = config['interface8'].get('mac', '')
interface8_enabled = config['interface8'].getboolean('enabled', False)
else:
interface8_enabled = False
# interface9 settings
if 'interface9' in config:
interface9_type = config['interface9'].get('type', 'serial')
port9 = config['interface9'].get('port', '')
hostname9 = config['interface9'].get('hostname', '')
mac9 = config['interface9'].get('mac', '')
interface9_enabled = config['interface9'].getboolean('enabled', False)
else:
interface9_enabled = False
multiple_interface = False
if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled:
multiple_interface = True
# variables from the config.ini file
try:
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
@@ -104,6 +193,7 @@ try:
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
store_forward_enabled = config['general'].getboolean('StoreForward', True)
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
@@ -111,15 +201,23 @@ try:
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
motd_enabled = config['general'].getboolean('motdEnabled', True)
MOTD = config['general'].get('motd', MOTD)
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
whoami_enabled = config['general'].getboolean('whoami', True)
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
bee_enabled = config['general'].getboolean('bee', False) # 🐝 off by default undocumented
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
# emergency response
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
emergency_responder_alert_interface = config['emergencyHandler'].getint('alert_interface', 1) # default 1
emergency_responder_email = config['emergencyHandler'].get('email', '').split(',')
# sentry
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
@@ -127,6 +225,7 @@ try:
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
# location
location_enabled = config['location'].getboolean('enabled', True)
@@ -134,28 +233,56 @@ try:
longitudeValue = config['location'].getfloat('lon', -123.0)
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
# location alerts
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
mySAME = config['location'].get('mySAME', '').split(',') # default empty
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
# E-Mail Settings
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
enableImap = config['smtp'].getboolean('enableImap', False)
SMTP_SERVER = config['smtp'].get('SMTP_SERVER', 'smtp.gmail.com')
SMTP_PORT = config['smtp'].getint('SMTP_PORT', 587)
FROM_EMAIL = config['smtp'].get('FROM_EMAIL', 'none@gmail.com')
SMTP_AUTH = config['smtp'].getboolean('SMTP_AUTH', True)
SMTP_USERNAME = config['smtp'].get('SMTP_USERNAME', FROM_EMAIL)
SMTP_PASSWORD = config['smtp'].get('SMTP_PASSWORD', 'password')
EMAIL_SUBJECT = config['smtp'].get('EMAIL_SUBJECT', 'Meshtastic✉')
IMAP_SERVER = config['smtp'].get('IMAP_SERVER', 'imap.gmail.com')
IMAP_PORT = config['smtp'].getint('IMAP_PORT', 993)
IMAP_USERNAME = config['smtp'].get('IMAP_USERNAME', SMTP_USERNAME)
IMAP_PASSWORD = config['smtp'].get('IMAP_PASSWORD', SMTP_PASSWORD)
IMAP_FOLDER = config['smtp'].get('IMAP_FOLDER', 'inbox')
# repeater
repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
# scheduler
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
@@ -166,9 +293,13 @@ try:
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
@@ -183,6 +314,8 @@ try:
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:
print(f"System: Error reading config file: {e}")

266
modules/smtp.py Normal file
View File

@@ -0,0 +1,266 @@
# SMTP module for the meshing-around bot
# 2024 Idea and code bits from https://github.com/tremmert81
# https://avtech.com/articles/138/list-of-email-to-sms-addresses/
# 2024 Kelly Keeton K7MHI
from modules.log import *
import pickle
import time
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# System variables
trap_list_smtp = ("email:", "setemail", "sms:", "setsms", "clearsms")
smtpThrottle = {}
SMTP_TIMEOUT = 10
if enableImap:
# Import IMAP library
import imaplib
import email
# Send email
def send_email(to_email, message, nodeID=0):
global smtpThrottle
# Clean up email address
to_email = to_email.strip()
# Basic email validation
if "@" not in to_email or "." not in to_email:
logger.warning(f"System: Invalid email address format: {to_email}")
return False
# throttle email to prevent abuse
if to_email in smtpThrottle:
if smtpThrottle[to_email] > time.time() - 120:
logger.warning("System: Email throttled for " + to_email[:-6])
return "Email throttled, try again later"
smtpThrottle[to_email] = time.time()
# check if email is in the ban list
if nodeID in bbs_ban_list:
logger.warning("System: Email blocked for " + str(nodeID))
return "Email throttled, try again later"
# Send email
try:
# Create message
msg = MIMEMultipart()
msg['From'] = FROM_EMAIL
msg['To'] = to_email
msg['Subject'] = EMAIL_SUBJECT
msg.attach(MIMEText(message, 'plain'))
# Connect to SMTP server
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=SMTP_TIMEOUT)
try:
# login /auth
if SMTP_PORT == 587:
server.starttls()
if SMTP_AUTH:
server.login(SMTP_USERNAME, SMTP_PASSWORD)
except Exception as e:
logger.warning(f"System: Failed to login to SMTP server: {str(e)}")
return
# Send email; this command will hold the program until the email is sent
server.send_message(msg)
server.quit()
logger.info("System: Email sent to: " + to_email[:-6])
return True
except Exception as e:
logger.warning(f"System: Failed to send email: {str(e)}")
return False
def check_email(nodeID, sysop=False):
if not enableImap:
return
try:
# Connect to IMAP server
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=SMTP_TIMEOUT)
mail.login(IMAP_USERNAME, IMAP_PASSWORD)
mail.select(IMAP_FOLDER)
# Search for new emails
status, data = mail.search(None, 'UNSEEN')
if status == 'OK':
for num in data[0].split():
status, data = mail.fetch(num, '(RFC822)')
if status == 'OK':
email_message = email.message_from_bytes(data[0][1])
email_from = email_message['from']
email_subject = email_message['subject']
email_body = ""
if not sysop:
# Check if email is whitelisted by particpant in the mesh
for address in sms_db[nodeID]:
if address in email_from:
email_body = email_message.get_payload()
logger.info("System: Email received from: " + email_from[:-6] + " for " + str(nodeID))
return email_body.strip()
else:
# Check if email is from sysop
for address in sysopEmails:
if address in email_from:
email_body = email_message.get_payload()
logger.info("System: SysOp Email received from: " + email_from[:-6] + " for sysop")
return email_body.strip()
except Exception as e:
logger.warning("System: Failed to check email: " + str(e))
return False
# initalize email db
email_db = {}
try:
with open('data/email_db.pickle', 'rb') as f:
email_db = pickle.load(f)
except:
logger.warning("System: Email db not found, creating a new one")
with open('data/email_db.pickle', 'wb') as f:
pickle.dump(email_db, f)
def store_email(nodeID, email):
global email_db
# if not in db, add it
logger.debug("System: Setting E-Mail for " + str(nodeID))
email_db[nodeID] = email
# save to a pickle for persistence, this is a simple db, be mindful of risk
with open('data/email_db.pickle', 'wb') as f:
pickle.dump(email_db, f)
f.close()
return True
# initalize SMS db
sms_db = [{'nodeID': 0, 'sms':[]}]
try:
with open('data/sms_db.pickle', 'rb') as f:
sms_db = pickle.load(f)
except:
logger.warning("System: SMS db not found, creating a new one")
with open('data/sms_db.pickle', 'wb') as f:
pickle.dump(sms_db, f)
def store_sms(nodeID, sms):
global sms_db
try:
logger.debug("System: Setting SMS for " + str(nodeID))
# if not in db, add it
if nodeID not in sms_db:
sms_db.append({'nodeID': nodeID, 'sms': sms})
else:
# if in db, update it
for item in sms_db:
if item['nodeID'] == nodeID:
item['sms'].append(sms)
# save to a pickle for persistence, this is a simple db, be mindful of risk
with open('data/sms_db.pickle', 'wb') as f:
pickle.dump(sms_db, f)
f.close()
return True
except Exception as e:
logger.warning("System: Failed to store SMS: " + str(e))
return False
def handle_sms(nodeID, message):
global sms_db
# if clearsms, remove all sms for node
if message.lower().startswith("clearsms"):
if any(item['nodeID'] == nodeID for item in sms_db):
# remove record from db for nodeID
sms_db = [item for item in sms_db if item['nodeID'] != nodeID]
# update the pickle
with open('data/sms_db.pickle', 'wb') as f:
pickle.dump(sms_db, f)
f.close()
return "📲 address cleared"
return "📲No address to clear"
# send SMS to SMS in db. if none ask for one
if message.lower().startswith("setsms"):
message = message.split(" ", 1)
if len(message[1]) < 5:
return "?📲setsms: example@phone.co"
if "@" not in message[1] and "." not in message[1]:
return "📲Please provide a valid email address"
if store_sms(nodeID, message[1]):
return "📲SMS address set 📪"
else:
return "Failed to set address"
if message.lower().startswith("sms:"):
message = message.split(" ", 1)
if any(item['nodeID'] == nodeID for item in sms_db):
count = 0
# for all dict items maching nodeID in sms_db send sms
for item in sms_db:
if item['nodeID'] == nodeID:
smsEmail = item['sms']
logger.info("System: Sending SMS for " + str(nodeID) + " to " + smsEmail[:-6])
if send_email(smsEmail, message[1], nodeID):
count += 1
else:
return "Failed to send SMS"
return "📲SMS sent " + str(count) + " addresses 📤"
else:
return "📲No address set, use 📲setsms"
return "Error: ⛔️ not understood. use:setsms example@phone.co"
def handle_email(nodeID, message):
global email_db
try:
# send email to email in db. if none ask for one
if message.lower().startswith("setemail"):
message = message.split(" ", 1)
if len(message) < 2:
return "📧Please provide an email address"
email_addr = message[1].strip()
if "@" not in email_addr or "." not in email_addr:
return "📧Please provide a valid email address"
if store_email(nodeID, email_addr):
return "📧Email address set 📪"
return "Error: ⛔️ Failed to set email address"
if message.lower().startswith("email:"):
parts = message.split(" ", 1)
if len(parts) < 2:
return "Error: ⛔️ format should be: email: message or, email: address@example.com #message"
content = parts[1].strip()
# Check if this is a direct email with address
if "@" in content and "#" in content:
# Split into email and message
addr_msg = content.split("#", 1)
if len(addr_msg) != 2:
return "Error: ⛔️ Message format should be: email: address@example.com #message"
to_email = addr_msg[0].strip()
message_body = addr_msg[1].strip()
logger.info(f"System: Sending email for {nodeID} to {to_email}")
if send_email(to_email, message_body, nodeID):
return "📧Email-sent 📤"
return "Failed to send email"
# Using stored email address
elif nodeID in email_db:
logger.info(f"System: Sending email for {nodeID} to stored address")
if send_email(email_db[nodeID], content, nodeID):
return "📧Email-sent 📤"
return "Failed to send email"
return "Error: ⛔️ no email on file. use: setemail"
except Exception as e:
logger.error(f"System: Email handling error: {str(e)}")
return "Failed to process email command"

View File

@@ -9,7 +9,7 @@ import ephem # pip install pyephem
from datetime import timedelta
from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond")
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
def hf_band_conditions():
# ham radio HF band conditions
@@ -140,3 +140,43 @@ def get_moon(lat=0, lon=0):
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
return moon_data
def getNextSatellitePass(satellite, lat=0, lon=0):
pass_data = ''
# get the next satellite pass for a given satellite
visualPassAPI = "https://api.n2yo.com/rest/v1/satellite/visualpasses/"
if lat == 0 and lon == 0:
lat = latitudeValue
lon = longitudeValue
# API URL
if n2yoAPIKey == '':
logger.error("System: Missing API key free at https://www.n2yo.com/login/")
return "not configured, bug your sysop"
url = visualPassAPI + str(satellite) + "/" + str(lat) + "/" + str(lon) + "/0/2/300/" + "&apiKey=" + n2yoAPIKey
# get the next pass data
try:
if not int(satellite):
raise Exception("Invalid satellite number")
next_pass_data = requests.get(url, timeout=urlTimeoutSeconds)
if(next_pass_data.ok):
pass_json = next_pass_data.json()
if 'info' in pass_json and 'passescount' in pass_json['info'] and pass_json['info']['passescount'] > 0:
satname = pass_json['info']['satname']
pass_time = pass_json['passes'][0]['startUTC']
pass_duration = pass_json['passes'][0]['duration']
pass_maxEl = pass_json['passes'][0]['maxEl']
pass_rise_time = datetime.fromtimestamp(pass_time).strftime('%a %d %I:%M%p')
pass_startAzCompass = pass_json['passes'][0]['startAzCompass']
pass_set_time = datetime.fromtimestamp(pass_time + pass_duration).strftime('%a %d %I:%M%p')
pass__endAzCompass = pass_json['passes'][0]['endAzCompass']
pass_data = f"{satname} @{pass_rise_time} Az:{pass_startAzCompass} for{getPrettyTime(pass_duration)}, MaxEl:{pass_maxEl}° Set@{pass_set_time} Az:{pass__endAzCompass}"
elif pass_json['info']['passescount'] == 0:
satname = pass_json['info']['satname']
pass_data = f"{satname} has no upcoming passes"
else:
logger.error(f"System: Error fetching satellite pass data {satellite}")
pass_data = ERROR_FETCHING_DATA
except Exception as e:
logger.warning(f"System: User supplied value {satellite} unknown or invalid")
pass_data = "Provide NORAD# example use:🛰satpass 25544,33591"
return pass_data

File diff suppressed because it is too large Load Diff

51
modules/web.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# This is a simple web server that serves up the content of the webRoot directory
# The reporting data is all that is currently being served up
# TODO - add interaction to mesh?
# to use this today run it seperately and open a browser to http://localhost:8420
import os
import http.server
# Set the port for the server
PORT = 8420
# set webRoot index.html location
webRoot = "etc/www"
# 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
if SSL:
import ssl
# disable logging
class QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
if webServerLogs:
super().log_message(format, *args)
# Change the current working directory to webRoot
os.chdir(webRoot)
# boot up simple HTTP server
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
if SSL:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
try:
ctx.load_cert_chain(certfile='./server.pem')
except FileNotFoundError:
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
exit(1)
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print(f"Serving reports at 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
httpd.serve_forever()
exit(0)

View File

@@ -1,18 +1,19 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#import requests_cache
#import openmeteo_requests # pip install openmeteo-requests
#from retry_requests import retry # pip install retry_requests
import requests
import json
from modules.log import *
def get_weather_data(api_url, params):
response = requests.get(api_url, params=params)
response.raise_for_status() # Raise an error for bad status codes
return response.json()
def get_wx_meteo(lat=0, lon=0, unit=0):
# set forcast days 1 or 3
forecastDays = 3
# Setup the Open-Meteo API client with cache and retry on error
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
retry_session = retry(retries = 3, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
@@ -34,27 +35,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
weather_data = get_weather_data(url, params)
except Exception as e:
logger.error(f"Error fetching meteo weather data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
response = responses[0]
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
# Process location
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
# Ensure response is defined
response = weather_data
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
daily = response['daily']
daily_weather_code = daily['weather_code']
daily_temperature_2m_max = daily['temperature_2m_max']
daily_temperature_2m_min = daily['temperature_2m_min']
daily_precipitation_hours = daily['precipitation_hours']
daily_precipitation_probability_max = daily['precipitation_probability_max']
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
@@ -191,3 +194,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
return weather_report
def get_flood_openmeteo(lat=0, lon=0):
# set forcast days 1 or 3
forecastDays = 3
# Flood data
url = "https://flood-api.open-meteo.com/v1/flood"
params = {
"latitude": {lat},
"longitude": {lon},
"timezone": "auto",
"daily": "river_discharge",
"forecast_days": forecastDays
}
try:
# Fetch the flood data
flood_data = get_weather_data(url, params)
except Exception as e:
logger.error(f"Error fetching meteo flood data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
# Ensure response is defined
response = flood_data
# Process daily data. The order of variables needs to be the same as requested.
daily = response['daily']
daily_river_discharge = daily['river_discharge']
# check if none
except Exception as e:
logger.error(f"Error processing meteo flood data: {e}")
return ERROR_FETCHING_DATA
# create a flood report
flood_report = ""
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
return flood_report

1
news.txt Normal file
View File

@@ -0,0 +1 @@
no new news is good news!

View File

@@ -2,30 +2,42 @@
# Meshtastic Autoresponder PONG Bot
# K7MHI Kelly Keeton 2024
try:
from pubsub import pub
except ImportError:
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh pong' using a virtual environment.")
exit(1)
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
import random
from modules.log import *
from modules.system import *
# Global Variables
DEBUGpacket = False # Debug print the packet rx
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
# Auto response to messages
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓Ping!!",
"motd": lambda: handle_motd(message, MOTD),
# 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,
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(hop, snr, rssi),
"test": lambda: handle_testing(hop, snr, rssi),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"motd": lambda: handle_motd(message, MOTD),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
}
cmds = [] # list to hold the commands found in the message
for key in command_handler:
@@ -44,17 +56,89 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
return bot_response
def handle_ping(message, hop, snr, rssi):
if "@" in message:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
msg = ""
type = ''
if "ping" in message.lower():
msg = "🏓PONG\n"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
"🎙Testing, testing\n",\
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
"🎙Roger that!\n",])
type = "🎙TEST"
elif "ack" in message.lower():
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
msg = f"QSP QSL OM DE {myname} K\n"
else:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
else:
msg = msg + hop
if "@" in message:
msg = msg + " @" + message.split("@")[1]
type = type + " @" + message.split("@")[1]
elif "#" in message:
msg = msg + " #" + message.split("#")[1]
type = type + " #" + message.split("#")[1]
# check for multi ping request
if " " in message:
# if stop multi ping
if "stop" in message.lower():
for i in range(0, len(multiPingList)):
if multiPingList[i].get('message_from_id') == message_from_id:
multiPingList.pop(i)
msg = "🛑 auto-ping"
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
return "🏓PONG, " + hop
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
def handle_motd(message):
global MOTD
@@ -64,65 +148,47 @@ def handle_motd(message):
return "MOTD Set to: " + MOTD
else:
return MOTD
def sysinfo(message, message_from_id, deviceID):
if "?" in message:
return "sysinfo command returns system information."
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
# display last heard nodes add to response
bot_response = "Last Heard\n"
bot_response += str(get_node_list(1))
# show last users of the bot with the cmdHistory list
history = handle_history(message, nodeid, deviceID, isDM, lheard=True)
if history:
bot_response += f'LastSeen\n{history}'
else:
# trim the last \n
bot_response = bot_response[:-1]
# bot_response += getNodeTelemetry(deviceID)
return bot_response
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "✋ACK-ACK! " + hop
def handle_testing(hop, snr, rssi):
if hop == "Direct":
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🎙Testing 1,2,3 " + hop
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxInterface}")
if port1 in rxInterface:
retry_int1 = True
elif interface2_enabled and port2 in rxInterface:
retry_int2 = True
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxHost}")
if hostname1 in rxHost and interface1_type == 'tcp':
retry_int1 = True
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
retry_int2 = True
if rxType == 'BLEInterface':
logger.critical(f"System: Lost Connection to Device BLE")
if interface1_type == 'ble':
retry_int1 = True
elif interface2_enabled and interface2_type == 'ble':
retry_int2 = True
def onReceive(packet, interface):
# extract interface defailts from interface object
global seenNodes
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
# extract interface details from inbound packet
rxType = type(interface).__name__
rxNode = 0
message_from_id = 0
snr = 0
rssi = 0
hop = 0
hop_away = 0
# Valies assinged to the packet
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
pkiStatus = (False, 'ABC')
replyIDset = False
emojiSeen = False
isDM = False
if DEBUGpacket:
# Debug print the interface object
for item in interface.__dict__.items(): intDebug = f"{item}\n"
@@ -130,62 +196,93 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface:
rxNode = 1
elif interface2_enabled and port2 in rxInterface:
rxNode = 2
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp':
rxNode = 1
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# check for a message packet and process it
# set the message_from_id
message_from_id = packet['from']
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
try:
snr = packet['rxSnr']
rssi = packet['rxRssi']
except KeyError:
snr = 0
rssi = 0
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet['hopsAway']
hop_away = packet.get('hopsAway', 0)
else:
# if the packet does not have a hop count try other methods
hop_away = 0
if packet.get('hopLimit'):
hop_limit = packet['hopLimit']
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet['hopStart']
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0:
hop = "MQTT"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
@@ -196,46 +293,53 @@ def onReceive(packet, interface):
hop = f"{hop_count} hops"
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
# log the message to the message log
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
time.sleep(responseDelay)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
if ignoreDefaultChannel and channel_number == publicChannel:
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
else:
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history list
@@ -257,45 +361,63 @@ def onReceive(packet, interface):
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
if repeater_enabled and multiple_interface:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
send_message(rMsg, channel_number, 0, i)
time.sleep(responseDelay)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False):
myNodeNum = globals().get(f'myNodeNum{i}', 0)
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
if log_messages_to_file:
logger.debug("System: Logging Messages to disk")
if syslog_to_file:
logger.debug("System: Logging System Logs to disk")
if solar_conditions_enabled:
logger.debug("System: Celestial Telemetry Enabled")
if motd_enabled:
logger.debug(f"System: MOTD Enabled using {MOTD}")
if sentry_enabled:
logger.debug("System: Sentry Enabled")
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug("System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and multiple_interface:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if file_monitor_enabled:
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
if read_news_enabled:
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Reminder Scheduler is enabled every Monday at noon send a log message
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
# here we go loopty loo
while True:
@@ -306,14 +428,19 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
await asyncio.wait([meshRxTask, watchdogTask])
if file_monitor_enabled:
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
await asyncio.gather(meshRxTask, watchdogTask)
if file_monitor_enabled:
await asyncio.gather(fileMonTask)
await asyncio.sleep(0.01)
try:
asyncLoop = asyncio.new_event_loop()
if __name__ == "__main__":
asyncio.run(main())
except KeyboardInterrupt:
exit_handler()
pass
# EOF

View File

@@ -3,15 +3,10 @@ pubsub
datetime
pyephem
requests
geopy
maidenhead
beautifulsoup4
dadjokes
openmeteo_requests
retry_requests
numpy
geopy
schedule
wikipedia
ollama
googlesearch-python

20
script/docker/README.md Normal file
View File

@@ -0,0 +1,20 @@
# How do I use this thing?
This is not a full turnkey setup for Docker yet but gets you most of the way there!
## Setup New Image
`docker build -t meshing-around .`
## Ollama Image with compose
still a WIP
`docker compose up -d`
## Edit the config.ini in the docker
To edit the config.ini in the docker you can
`docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"`
## other info
1. Ensure your serial port is properly shared.
2. Run the Docker container:
```sh
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
```

View File

@@ -0,0 +1,52 @@
services:
meshing-around:
build:
context: ../..
depends_on:
ollama:
condition: service_healthy
devices:
- /dev/ttyAMA10 # Replace this with your actual device!
configs:
- source: me_config
target: /app/config.ini
extra_hosts:
- "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp
ollama:
image: ollama/ollama:0.5.1
volumes:
- ./ollama:/root/.ollama
- ./ollama-entrypoint.sh:./entrypoint.sh
container_name: ollama
pull_policy: always
tty: true
restart: always
entrypoint:
- /usr/bin/bash
- /script/docker/entrypoint.sh
expose:
- 11434
healthcheck:
test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
interval: 30s
timeout: 10s
retries: 20
node-exporter:
image: quay.io/prometheus/node-exporter:latest
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- --path.procfs=/host/proc
- --path.rootfs=/rootfs
- --path.sysfs=/host/sys
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
restart: unless-stopped
expose:
- 9100
network_mode: host
pid: host
configs:
me_config:
file: ./config.ini

View File

@@ -0,0 +1,6 @@
REM batch file to install docker on windows
REM docker compose up -d
cd ../../
docker build -t meshing-around .
REM docker-compose up -d
docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"

View File

@@ -0,0 +1,2 @@
REM launch meshing-around container with a terminal
docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"

View File

@@ -0,0 +1,6 @@
#!/bin/bash
# instruction set the meshing-around docker container entrypoint
# Substitute environment variables in the config file (what is the purpose of this?)
# envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
# Run the bot
exec python /app/mesh_bot.py

View File

@@ -0,0 +1,16 @@
#!/bin/bash
# Start Ollama in the background.
/bin/ollama serve &
# Record Process ID.
pid=$!
# Pause for Ollama to start.
sleep 5
echo "🔴 Retrieve llama3.2:3b model..."
ollama pull llama3.2:3b
echo "🟢 Done!"
# Wait for Ollama process to finish.
wait $pid

7
script/runShell.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# meshing-around demo script for shell scripting
# runShell.sh
cd "$(dirname "$0")"
program_path=$(pwd)
printf "Running meshing-around demo script for shell scripting from $program_path\n"

27
script/sysEnv.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# meshing-around shell script for sysinfo
# runShell.sh
cd "$(dirname "$0")"
program_path=$(pwd)
# get basic telemetry data. Free space, CPU, RAM, and temperature for a raspberry pi
free_space=$(df -h | grep ' /$' | awk '{print $4}')
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
ram_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}')
ram_free=$(echo "scale=2; 100 - $ram_usage" | bc)
# if command vcgencmd is found, part of raspberrypi tools, use it to get temperature
if command -v vcgencmd &> /dev/null
then
# get temperature
temp=$(vcgencmd measure_temp | sed "s/temp=//" | sed "s/'C//")
# temp in fahrenheit
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
else
# get temperature from thermal zone
temp=$(paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | grep "temp" | awk '{print $2/1000}' | awk '{s+=$1} END {print s/NR}')
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
fi
# print telemetry data rounded to 2 decimal places
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"