Compare commits

...

264 Commits

Author SHA1 Message Date
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
Kelly
eeab9f3fb1 Merge pull request #86 from SpudGunMan/lab
Enhancements 🦃
2024-11-27 23:01:45 -08:00
SpudGunMan
c21a67d1cf Update README.md 2024-11-27 22:58:47 -08:00
SpudGunMan
afe48a44da fixEAS Multi Channel 2024-11-27 22:50:30 -08:00
SpudGunMan
7e4822e4ec Update locationdata.py 2024-11-27 22:22:19 -08:00
SpudGunMan
705ab6a980 fixClosesNodex2 2024-11-27 21:31:52 -08:00
SpudGunMan
963b29eea4 fixEnhanceAutoPing 2024-11-27 21:20:08 -08:00
SpudGunMan
b3f889c4c7 fixBugClosestNodes 2024-11-27 20:40:08 -08:00
SpudGunMan
545b4891b4 error2Warning 2024-11-27 20:38:38 -08:00
SpudGunMan
c89f14b3c2 fix that needed for later 2024-11-25 11:51:25 -08:00
SpudGunMan
c416b00383 newLogRotation 2024-11-25 11:49:39 -08:00
SpudGunMan
669a891eeb Update log.py 2024-11-25 11:46:22 -08:00
SpudGunMan
520d58b262 Update log.py 2024-11-25 11:35:36 -08:00
SpudGunMan
24dff868ff RotateLogger
default is 32 days of logs configure if needed otherwise.
2024-11-24 19:52:25 -08:00
SpudGunMan
cf45bb5060 LOG Rotation Update
update log handler
2024-11-24 19:43:47 -08:00
SpudGunMan
0f9064f2c3 EAS API Alerts
Enable EAS API Messages to Mesh
Fix multiping Device 2
2024-11-23 16:27:55 -08:00
SpudGunMan
f94f329b1f Create eas_alert_parser.py 2024-11-23 15:50:04 -08:00
SpudGunMan
dc4560081d dropLangChain 2024-11-23 15:49:27 -08:00
SpudGunMan
b42cd0e6dc Update README.md 2024-11-20 16:06:28 -08:00
SpudGunMan
bbe1e45541 Update README.md 2024-11-20 16:03:22 -08:00
SpudGunMan
2c61db1215 fix that enhance 2024-11-19 19:52:04 -08:00
SpudGunMan
fde2bb94d9 enhance 2024-11-19 19:51:40 -08:00
SpudGunMan
436a43d3ad Update README.md 2024-11-19 19:49:46 -08:00
SpudGunMan
6b2a6f3a83 enhanceFileMon 2024-11-19 19:44:49 -08:00
SpudGunMan
8e5773115c FileMon
Enhancement with FileMon to watch a file and deliver its goods to the mesh
2024-11-19 19:41:14 -08:00
SpudGunMan
626a5dfe16 moveAPItide 2024-11-15 16:21:56 -08:00
SpudGunMan
e63f4816c4 Update locationdata.py 2024-11-15 14:16:35 -08:00
SpudGunMan
13852b194b enhance 2024-11-11 15:11:25 -08:00
SpudGunMan
a68c20098b ScrubUno
gone but not forgotten
2024-11-11 14:41:14 -08:00
Kelly
432b5a767e Merge pull request #85 from SpudGunMan/lab
BBS LiNK
2024-11-07 15:01:54 -08:00
SpudGunMan
952659198c fixes 2024-11-05 07:45:19 -08:00
SpudGunMan
4e518758e5 Update bbstools.py 2024-10-31 16:56:43 -07:00
SpudGunMan
e1b3dd311f Update bbstools.py 2024-10-31 16:42:09 -07:00
SpudGunMan
bb0f923155 bbslink
first attempt at giving BBS link over the air
2024-10-31 16:38:16 -07:00
SpudGunMan
ab86f02bd7 Update llm.py 2024-10-27 17:12:28 -07:00
SpudGunMan
43067cfb07 Update llm.py 2024-10-27 16:58:05 -07:00
SpudGunMan
3300694059 Update mesh_bot.py 2024-10-26 18:54:48 -07:00
SpudGunMan
f59b8715dd Update simulator.py 2024-10-26 17:31:05 -07:00
SpudGunMan
60abadd1fc Update llm.py 2024-10-24 19:49:01 -07:00
SpudGunMan
4297c91c5e Update llm.py 2024-10-24 19:47:19 -07:00
SpudGunMan
c8eddc3787 Update llm.py 2024-10-24 19:44:51 -07:00
SpudGunMan
d01d81a6d7 openWebUI 2024-10-24 19:21:23 -07:00
SpudGunMan
40b31fd8af Update dopewar.py 2024-10-23 22:13:48 -07:00
SpudGunMan
7b995b35cd 💊
cleanup display on some things
2024-10-23 22:07:46 -07:00
SpudGunMan
00885d57c9 Update dopewar.py 2024-10-23 21:21:44 -07:00
SpudGunMan
d03d7dbc47 Update llm.py 2024-10-21 19:36:02 -07:00
SpudGunMan
7fd4074bd3 Update videopoker.py 2024-10-20 22:56:58 -07:00
SpudGunMan
8367bca4d5 Update joke.py 2024-10-20 17:42:00 -07:00
SpudGunMan
5059990adb Update mesh_bot.py 2024-10-20 17:24:22 -07:00
SpudGunMan
9dd9d39df4 Update llm.py 2024-10-19 08:14:23 -07:00
SpudGunMan
87f89fea6d Update llm.py 2024-10-18 10:50:29 -07:00
28 changed files with 1985 additions and 732 deletions

287
README.md
View File

@@ -1,14 +1,19 @@
# 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
### 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,11 +24,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.
- **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.
- **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.
@@ -36,6 +44,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.
### 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.
### File Monitor Alerts
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
- **News File**: On request of news, the contents of the file are returned.
### Data Reporting
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
@@ -43,11 +60,12 @@ 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, or [femtofox](https://github.com/noon92/femtofox) project for embedding, possibly see the [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic). 🥔 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
```
@@ -60,7 +78,7 @@ The code is under active development, so make sure to pull the latest changes re
#### 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).
1. Ensure your serial port is properly shared and the GPU is configured if using LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
2. Build the Docker image:
```sh
cd meshing-around
@@ -109,16 +127,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, but for locations outside the USA, you can set `UseMeteoWxAPI` "Go to definition") 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. It is also the default (or value when none found for user) for Sentry, all NOAA, repeater lookup, etc.
```ini
[location]
@@ -150,18 +169,74 @@ 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/IMAP.
```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]
# enable or disable the emergency response handler
enabled = True
# channel to send a message to when the emergency handler is triggered
alert_channel = 2
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
This uses the SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. femaAlertBroadcastCh is currently not written, still under development.
```ini
# FEMA IPAWS/CAP Alert Broadcast
femaAlertBroadcastEnabled = True
# FEMA IPAWS/CAP Alert Broadcast Channels
femaAlertBroadcastCh = 2,4
# Ignore any headline that includes the word Test
ignoreFEMAtest = True
# comma separated list of codes trigger local alert. (e.g., SAME, FIPS, ZIP)
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
```
#### 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
@@ -169,22 +244,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
@@ -202,8 +263,65 @@ llmContext_fromGoogle = True # enable context from google search results helps w
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
### 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]
filemon_enabled = True
file_path = alert.txt
broadcastCh = 2,4
enable_read_news = False
news_file_path = news.txt
```
#### 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), [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
```bash
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
```
The following example shell command will pipe rtl_sdr to alert.txt
```bash
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
### 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
@@ -213,62 +331,30 @@ schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'),
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
```
#### BBS Link
The scheduler also handles the BBL 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.
### 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 langchain
pip install langchain-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 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. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
## 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) | ✅ |
| `test` | Returns like ping but also can be used to test the limits of data buffers `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `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) | ✅ |
@@ -282,8 +368,9 @@ sudo apt-get install fonts-noto-color-emoji
| `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 |
| `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 | |
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts. Headline or expanded details | |
### Bulletin Board & Mail
| Command | Description | |
@@ -294,6 +381,12 @@ 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) | ✅ |
| `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 | |
@@ -301,8 +394,8 @@ sudo apt-get install fonts-noto-color-emoji
| `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`| |
### Games (via DM)
| Command | Description | |
@@ -314,7 +407,6 @@ sudo apt-get install fonts-noto-color-emoji
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `uno` | Plays Uno card game against the bot or with others on the mesh near you! | ✅ |
# Recognition
@@ -337,11 +429,52 @@ 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.
- **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 (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 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 googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```

View File

@@ -25,58 +25,69 @@ port = /dev/ttyUSB0
[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 DM
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
# 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
uno = 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
@@ -92,22 +103,77 @@ 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 =
[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
# 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
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# FEMA IPAWS/CAP Alert Broadcast
femaAlertBroadcastEnabled = False
# FEMA IPAWS/CAP Alert Broadcast Channels
femaAlertBroadcastCh = 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
# 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]
@@ -116,7 +182,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
@@ -132,11 +202,34 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
[fileMon]
filemon_enabled = False
file_path = alert.txt
broadcastCh = 2
enable_read_news = False
news_file_path = news.txt
[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 lilmit Buffer for radio testing
maxBuffer = 220

View File

@@ -1,4 +1,5 @@
#!/bin/bash
# instruction set the meshing-around docker container
# Substitute environment variables in the config file
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini

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}")

49
etc/eas_alert_parser.py Normal file
View File

@@ -0,0 +1,49 @@
# Super sloppy multimon-ng output cleaner for processing by EAS2Text
# I maed dis, sorta, mostly just mashed code I found or that chatGPT hallucinated
# by Mike O'Connell/skrrt, no licence or whatever just be chill yo
# enhanced by sheer.cold
import re
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
inp=input().strip()
except EOFError:
break
# 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:
@@ -372,7 +372,7 @@ def get_database_info():
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
except Exception as e:
print(f"Error reading database file: {str(e)}")
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
lemon_score = "no data"
elif 'dopewar' in file:
@@ -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']),
@@ -922,8 +922,8 @@ def generate_database_html(database_info):
def main():
log_dir = LOG_PATH
today = datetime.now().strftime('%Y_%m_%d')
log_file = f'meshbot{today}.log'
today = datetime.now().strftime('%Y-%m-%d')
log_file = f'meshbot.log'
log_path = os.path.join(log_dir, log_file)
if not os.path.exists(log_path):

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:
@@ -381,7 +381,7 @@ def get_database_info():
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
except Exception as e:
print(f"Error reading database file: {str(e)}")
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
lemon_score = "no data"
elif 'dopewar' in file:
@@ -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']),
@@ -1217,8 +1217,8 @@ def generate_database_html(database_info):
def main():
# Log file
log_dir = LOG_PATH
today = datetime.now().strftime('%Y_%m_%d')
log_file = f'meshbot{today}.log'
today = datetime.now().strftime('%Y-%m-%d')
log_file = f'meshbot.log'
log_path = os.path.join(log_dir, log_file)
if not os.path.exists(log_path):

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
@@ -25,7 +25,7 @@ def get_name_from_number(nodeID, length='short', interface=1):
# # Function to handle, or the project in test
def example_handler(nodeID, message):
def example_handler(message, nodeID, deviceID):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "

View File

@@ -1,31 +1,51 @@
#!/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."
printf "Installer works best in raspian/debian/ubuntu, if there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# 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"
# 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
@@ -34,34 +54,42 @@ if [ -f config.ini ]; then
fi
cp config.template config.ini
printf "\nConfig file generated\n"
printf "\nConfig files generated!\n"
# 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)"
printf "\nDo you want to install the bot in a python 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"
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..."
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"
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
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
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
@@ -105,14 +133,6 @@ 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
# install service for pong bot
sudo cp etc/pong_bot.service /etc/systemd/system/
@@ -132,11 +152,19 @@ if [ $bot == "n" ]; then
fi
fi
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
# 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 [ $emoji == "y" ]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
# ask if the user wants to install the LLM Ollama components
echo "Do you want to install the LLM Ollama components? (y/n)"
printf "\nDo you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
@@ -150,10 +178,16 @@ if [ $ollama == "y" ]; then
fi
fi
echo "Good time to reboot? (y/n)"
if [ $venv == "y" ]; then
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
fi
printf "\nGood time to reboot? (y/n)"
read reboot
if [ $reboot == "y" ]; then
sudo reboot
fi
printf "\nInstallation complete!\n"
exit 0

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
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)

View File

@@ -10,12 +10,14 @@ from modules.log import *
from modules.system import *
# list of commands to remove from the default list for DM only
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "uno"]
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
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
@@ -25,21 +27,27 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# Command List
default_commands = {
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"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),
"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(),
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbslist": bbs_list_messages,
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
"bbsread": lambda: handle_bbsread(message),
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"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),
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"ea": lambda: handle_fema_alerts(message, message_from_id, deviceID),
"ealert": lambda: handle_fema_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),
@@ -52,28 +60,42 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
"motd": lambda: handle_motd(message, message_from_id, isDM),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"playuno": lambda: handleUno(message, message_from_id, deviceID),
"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(),
"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),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"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),
# 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
@@ -109,7 +131,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
time.sleep(responseDelay)
return bot_response
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
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"
@@ -119,7 +141,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
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",\
@@ -150,6 +172,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
msg = msg + " #" + message.split("#")[1]
type = type + " #" + message.split("#")[1]
# check for multi ping request
if " " in message:
# if stop multi ping
@@ -158,16 +181,36 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
if multiPingList[i].get('message_from_id') == message_from_id:
multiPingList.pop(i)
msg = "🛑 auto-ping"
try:
pingCount = int(message.split(" ")[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})
msg = f"🚦Initalizing {pingCount} auto-ping"
# disabled in channel
if autoPingInChannel and not isDM:
# 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
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"
else:
msg = "🔊AutoPing via DM only⛔"
# if not a DM add the username to the beginning of msg
if not isDM:
msg = get_name_from_number(message_from_id) + msg
return msg
@@ -175,6 +218,34 @@ 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):
# 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:
if deviceID == 1: rxNode = myNodeNum1
elif deviceID == 2: rxNode = myNodeNum2
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(rxNode, '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,8 +284,9 @@ def handle_wxalert(message_from_id, deviceID, message):
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
weatherAlert = weatherAlert[0]
if NO_ALERTS not in weatherAlert:
weatherAlert = weatherAlert[0]
return weatherAlert
def handle_wiki(message, isDM):
@@ -233,6 +305,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'
@@ -545,29 +645,6 @@ def handleGolf(message, nodeID, deviceID):
time.sleep(responseDelay + 1)
return msg
def handleUno(message, nodeID, deviceID):
global unoTracker
msg = ''
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
last_cmd = unoTracker[i]['cmd']
logger.debug(f"System: {nodeID} PlayingGame uno last_cmd: {last_cmd}")
if last_cmd == "" and nodeID != 0:
# create new player
logger.debug("System: Uno: New Player: " + str(nodeID) + " " + get_name_from_number(nodeID))
unoTracker.append({'nodeID': nodeID, 'last_played': time.time(), 'cmd': '', 'playerName': get_name_from_number(nodeID)})
msg = "Welcome to 🃏 Uno!, waiting for others to join, (S)tart when ready"
msg += playUno(nodeID, message=message)
# wait a second to keep from message collision
time.sleep(responseDelay + 1)
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:
@@ -584,6 +661,15 @@ def handle_wxc(message_from_id, deviceID, cmd):
weather = get_weather(str(location[0]), str(location[1]))
return weather
def handle_fema_alerts(message, message_from_id, deviceID):
location = get_node_location(message_from_id, deviceID)
if message.lower().startswith("ealert"):
# Detailed alert
return getIpawsAlert(str(location[0]), str(location[1]))
else:
# Headlines only
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]
@@ -665,6 +751,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
@@ -768,6 +857,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
@@ -798,7 +927,6 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
(jackTracker, "BlackJack", handleBlackJack),
(mindTracker, "MasterMind", handleMmind),
(golfTracker, "GolfSim", handleGolf),
(unoTracker, "Uno", handleUno)
]
for tracker, game_name, handle_game_func in trackers:
@@ -809,6 +937,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
@@ -818,6 +947,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:
@@ -827,7 +958,6 @@ 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')
@@ -853,9 +983,15 @@ def onReceive(packet, interface):
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:
@@ -871,7 +1007,6 @@ 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']
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
@@ -880,7 +1015,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'):
@@ -896,9 +1039,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:
@@ -909,7 +1061,7 @@ 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
@@ -919,7 +1071,7 @@ def onReceive(packet, interface):
# 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)}")
@@ -930,22 +1082,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:
@@ -1032,6 +1204,11 @@ async def start_rx():
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:
@@ -1057,9 +1234,25 @@ async def start_rx():
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 wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
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))
@@ -1076,13 +1269,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 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()
@@ -1095,11 +1292,17 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if file_monitor_enabled:
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:
await asyncio.wait([meshRxTask, watchdogTask])
await asyncio.gather(meshRxTask, watchdogTask)
if radio_detection_enabled:
await asyncio.gather(hamlibTask)
if file_monitor_enabled:
await asyncio.gather(fileMonTask)
await asyncio.sleep(0.01)
try:

View File

@@ -3,8 +3,9 @@
import pickle # pip install pickle
from modules.log import *
import time
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo")
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
# global message list, later we will use a pickle on disk
bbs_messages = []
@@ -77,6 +78,12 @@ def bbs_post_message(subject, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# validate not a duplicate message
for msg in bbs_messages:
if msg[1].strip().lower() == subject.strip().lower() and msg[2].strip().lower() == message.strip().lower():
messageID = msg[0]
return "Message posted. ID is: " + str(messageID)
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
@@ -156,6 +163,42 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
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:
#store the message
subject = input.split("$")[1].split("#")[0]
body = input.split("#")[1]
bbs_post_message(subject, body, peerNode)
messageID = input.split(" ")[1]
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
# 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))
#initialize the bbsdb's
load_bbsdb()
load_bbsdm()

50
modules/filemon.py Normal file
View File

@@ -0,0 +1,50 @@
# File monitor module for the meshing-around bot
# 2024 Kelly Keeton K7MHI
from modules.log import *
import asyncio
import os
trap_list_filemon = ("readnews",)
def read_file(file_monitor_file_path):
try:
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
def read_news():
# read the news file on demand
return read_file(news_file_path)
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
else:
last_modified_time = os.path.getmtime(file_monitor_file_path)
while True:
current_modified_time = os.path.getmtime(file_monitor_file_path)
if current_modified_time != last_modified_time:
# File has been modified
content = read_file(file_monitor_file_path)
last_modified_time = current_modified_time
# Cleanup the content
content = content.replace('\n', ' ').replace('\r', '').strip()
if content:
return content
await asyncio.sleep(1) # Check every

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 " + 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)
@@ -232,14 +232,17 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1):
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(price_list[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(cost) + " "
buy_amount = value
if buy_amount == 'm':
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)
@@ -315,15 +318,17 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
" The going price is: $" + str(price_list[drug_choice])
" The going price is: $" + str("{:,}".format(cost))
# check if the user has enough of the drug to sell
if sell_amount <= amount[drug_choice]:
amount[drug_choice] -= sell_amount
cash += sell_amount * price_list[drug_choice]
inventory -= sell_amount
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name + ' for $' +\
str(sell_amount * price_list[drug_choice]) + '. Total cash: $' + "{:,}".format(cash)
profit = sell_amount * price_list[drug_choice]
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name +\
' for $' + "{:,}".format(profit) + '. Total cash: $' + "{:,}".format(cash)
else:
msg = "You don't have that much"
return msg
@@ -392,7 +397,7 @@ def endGameDw(nodeID):
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
with open('data/dopewar_hs.pkl', 'wb') as file:
pickle.dump(dwHighScore, file)
msg = "You finished with $" + str(cash) + " and beat the high score!🎉💰"
msg = "You finished with $" + "{:,}".format(cash) + " and beat the high score!🎉💰"
return msg
if cash > starting_cash:
msg = 'You made money! 💵 Up ' + str((cash/starting_cash).__round__()) + 'x! Well done.'
@@ -601,9 +606,9 @@ def playDopeWars(nodeID, cmd):
sell = sell_func(nodeID, price_list, i, 'm')
# ignore starts with "You don't have any"
if not sell.startswith("You don't have any"):
msg += sell
if i != len(my_drugs):
msg += '\n'
msg += sell + '\n'
# trim the last newline
msg = msg[:-1]
return msg
elif 'f' in menu_choice:
# set last command to location
@@ -614,7 +619,7 @@ def playDopeWars(nodeID, cmd):
elif 'p' in menu_choice:
# render_game_screen
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0)
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0, 'nothing')
return msg
elif 'e' in menu_choice:
msg = endGameDw(nodeID)

View File

@@ -78,14 +78,14 @@ def tableOfContents():
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'work': '💼', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨‍🏫', 'referee': '🧑‍⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉'
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
return wordToEmojiMap
@@ -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

View File

@@ -1,200 +0,0 @@
# https://github.com/melvin-02/UNO-game
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
from modules.log import *
color = ('RED', 'GREEN', 'BLUE', 'YELLOW')
rank = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Skip', 'Reverse', 'Draw2', 'Draw4', 'Wild')
ctype = {'0': 'number', '1': 'number', '2': 'number', '3': 'number', '4': 'number', '5': 'number', '6': 'number',
'7': 'number', '8': 'number', '9': 'number', 'Skip': 'action', 'Reverse': 'action', 'Draw2': 'action',
'Draw4': 'action_nocolor', 'Wild': 'action_nocolor'}
# Player List
unoLobby = []
unoTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'playerName': ''}]
unoGameTable = {'turn': -1, 'direction': 1, 'deck': None, 'hands': None, 'top_card': None}
class Card:
def __init__(self, color, rank):
self.rank = rank
if ctype[rank] == 'number':
self.color = color
self.cardtype = 'number'
elif ctype[rank] == 'action':
self.color = color
self.cardtype = 'action'
else:
self.color = None
self.cardtype = 'action_nocolor'
def __str__(self):
if self.color is None:
return self.rank
else:
return self.color + " " + self.rank
class Deck:
def __init__(self):
self.deck = []
self.discard_pile = []
for clr in color:
for ran in rank:
if ctype[ran] != 'action_nocolor':
self.deck.append(Card(clr, ran))
self.deck.append(Card(clr, ran))
else:
self.deck.append(Card(clr, ran))
def __str__(self):
deck_comp = ''
for card in self.deck:
deck_comp += '\n' + card.__str__()
return 'The deck has ' + deck_comp
def shuffle(self):
random.shuffle(self.deck)
def deal(self):
if not self.deck:
self.reshuffle_discard_pile()
return self.deck.pop()
def reshuffle_discard_pile(self):
if len(self.discard_pile) > 1:
top_card = self.discard_pile.pop()
self.deck = self.discard_pile[:]
self.discard_pile = [top_card]
self.shuffle()
else:
raise IndexError("No cards left to reshuffle")
class Hand:
def __init__(self):
self.cards = []
self.cardsstr = []
self.number_cards = 0
self.action_cards = 0
def add_card(self, card):
self.cards.append(card)
self.cardsstr.append(str(card))
if card.cardtype == 'number':
self.number_cards += 1
else:
self.action_cards += 1
self.sort_cards()
def remove_card(self, place):
self.cardsstr.pop(place - 1)
return self.cards.pop(place - 1)
def cards_in_hand(self):
msg = ''
for i in range(len(self.cardsstr)):
msg += f' {i + 1}.{self.cardsstr[i]}'
return msg
def single_card(self, place):
return self.cards[place - 1]
def no_of_cards(self):
return len(self.cards)
def sort_cards(self):
self.cards.sort(key=lambda card: (
card.color if card.color is not None else '',
int(card.rank) if card.cardtype == 'number' and card.rank is not None else 0))
self.cardsstr = [str(card) for card in self.cards]
def choose_first():
global unoLobby
if unoLobby != []:
random_player = random.choice(unoLobby)
return random_player
else:
return None
def single_card_check(top_card, card):
if card.color == top_card.color or top_card.rank == card.rank or card.cardtype == 'action_nocolor':
return True
else:
return False
def full_hand_check(hand, top_card):
for c in hand.cards:
if c.color == top_card.color or c.rank == top_card.rank or c.cardtype == 'action_nocolor':
#return hand.remove_card(hand.cardsstr.index(str(c)) + 1)
return hand.remove_card(hand.cards.index(c) + 1)
else:
return 'no card'
def win_check(hand):
if len(hand.cards) == 0:
return True
else:
return False
def last_card_check(hand):
for c in hand.cards:
if c.cardtype != 'number':
return True
else:
return False
def getNextPlayer(playerIndex, direction=1, skip=False):
current_index = unoLobby.index(playerIndex)
next_index = (current_index + direction) % len(unoLobby)
if skip:
next_index = (next_index + direction) % len(unoLobby)
return unoLobby[next_index]
def getNextPlayerID(playerIndex, direction=1, skip=False):
current_index = unoLobby.index(playerIndex)
next_index = (current_index + direction) % len(unoLobby)
if skip:
next_index = (next_index + direction) % len(unoLobby)
return unoTracker[next_index]['nodeID']
def unoPlayerDetail(nodeID):
for i in range(len(unoTracker)):
if unoTracker[i] == nodeID:
return f'{unoTracker[i]}'
def getUnoPname(nodeID):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
return unoTracker[i]['playerName']
def setLastCmd(nodeID, cmd):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
unoTracker[i]['cmd'] = cmd
def getLastCmd(nodeID):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
return unoTracker[i]['cmd']
def getUnoIDs():
global unoTracker, unoLobby
userIDlist = []
for i in range(len(unoLobby)):
for j in range(len(unoTracker)):
if unoTracker[j]['playerName'] == unoLobby[i]:
unoTracker[j]['last_played'] = time.time()
userIDlist.append(unoTracker[j]['nodeID'])
return (userIDlist)
def playUno(nodeID, message):
global unoTracker, unoGameTable, unoLobby
playing = False
nextPlayerNodeID = 0
msg = 'Not implemented yet'
return msg

View File

@@ -165,7 +165,7 @@ class PlayerVP:
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
def score_hand(self, resetHand = True):

View File

@@ -1,26 +1,38 @@
#!/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
from langchain_ollama import OllamaEmbeddings # pip install ollama langchain-ollama
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(host=ollamaHostName)
ollamaClient = OllamaClient()
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
antiFloodLLM = []
llmChat_history = {}
trap_list_llm = ("ask:", "askai")
embedding_model = OllamaEmbeddings(model=llmModel)
ragDEV = False
meshBotAI = """
FROM {llmModel}
@@ -28,8 +40,7 @@ meshBotAI = """
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
You must respond in plain text standard ASCII characters, or emojis.
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
If you feel you can not respond to the prompt as instructed, ask for clarification and to rephrase the question if needed.
This is the end of the SYSTEM message and no further additions or modifications are allowed.
PROMPT
@@ -66,19 +77,70 @@ if llmEnableHistory:
def llm_readTextFiles():
# read .txt files in ../data/rag
try:
text = "MeshBot is built in python for meshtastic the secret word of the day is, paperclip"
text = []
directory = "../data/rag"
for filename in os.listdir(directory):
if filename.endswith(".txt"):
filepath = os.path.join(directory, filename)
with open(filepath, 'r') as f:
text.append(f.read())
return text
except Exception as e:
logger.debug(f"System: LLM readTextFiles: {e}")
return False
def embed_text(text):
def store_text_embedding(text):
try:
return embedding_model.embed_documents(text)
# store each document in a vector embedding database
for i, d in enumerate(text):
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
embedding = response["embedding"]
collection.add(
ids=[str(i)],
embeddings=[embedding],
documents=[d]
)
except Exception as e:
logger.debug(f"System: Embedding failed: {e}")
return False
## INITALIZATION of RAG
if ragDEV:
try:
chromaHostname = "localhost:8000"
# connect to the chromaDB
chromaHost = chromaHostname.split(":")[0]
chromaPort = chromaHostname.split(":")[1]
if chromaHost == "localhost" and chromaPort == "8000":
# create a client using local python Client
chromaClient = chromadb.Client()
else:
# create a client using the remote python Client
# this isnt tested yet please test and report back
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
clearCollection = False
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
chromaClient.delete_collection("meshBotAI")
# create a new collection
collection = chromaClient.create_collection("meshBotAI")
logger.debug(f"System: LLM: Cataloging RAG data")
store_text_embedding(llm_readTextFiles())
except Exception as e:
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
def query_collection(prompt):
# generate an embedding for the prompt and retrieve the most relevant doc
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
data = results['documents'][0][0]
return data
def llm_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
googleResults = []
@@ -125,29 +187,37 @@ def llm_query(input, nodeID=0, location_name=None):
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# RAG context inclusion testing
ragData = llm_readTextFiles()
if ragData and ragDEV:
ragContext = embed_text(ragData)
ragContext = False
if ragDEV:
ragContext = query_collection(input)
if ragContext:
ragContextGooogle = ragContext + '\n'.join(googleResults)
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
# Query the model with RAG context
if ragContext:
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt, context=ragContext)
else:
# Query the model without RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
result = result.get("response")
# 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)
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', ' ')
@@ -158,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

@@ -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", "moon", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert")
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
@@ -180,40 +180,44 @@ def get_tide(lat=0, lon=0):
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
if zuluTime:
station_url += "&clock=24hour"
station_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&time_zone=lst_ldt&datum=MLLW&product=predictions&interval=hilo&format=json&station=" + station_id
if use_metric:
station_url += "&units=metric"
else:
station_url += "&units=english"
try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok:
logger.error("Location:Error fetching station data from NOAA")
tide_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if tide_data.ok:
tide_json = tide_data.json()
else:
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA
# extract table class="table table-condensed"
soup = bs.BeautifulSoup(station_data.text, 'html.parser')
table = soup.find('table', class_='table table-condensed')
# extract rows
rows = table.find_all('tr')
# extract data from rows
tide_data = []
for row in rows:
row_text = ""
cols = row.find_all('td')
for col in cols:
row_text += col.text + " "
tide_data.append(row_text)
# format tide data into a string
tide_string = ""
for data in tide_data:
tide_string += data + "\n"
# trim off last newline
tide_string = tide_string[:-1]
return tide_string
except (requests.exceptions.RequestException, json.JSONDecodeError):
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
tide_data = tide_json['predictions']
# format tide data into a table string for mesh
# get the date out of the first t value
tide_date = tide_data[0]['t'].split(" ")[0]
tide_table = "Tide Data for " + tide_date + "\n"
for tide in tide_data:
tide_time = tide['t'].split(" ")[1]
if not zuluTime:
# convert to 12 hour clock
if int(tide_time.split(":")[0]) > 12:
tide_time = str(int(tide_time.split(":")[0]) - 12) + ":" + tide_time.split(":")[1] + " PM"
else:
tide_time = tide_time + " AM"
tide_table += tide['type'] + " " + tide_time + ", " + tide['v'] + "\n"
# remove last newline
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
@@ -251,7 +255,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"
@@ -274,23 +278,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",
@@ -299,33 +303,45 @@ 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):
def getWeatherAlerts(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:
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -359,12 +375,31 @@ def getWeatherAlerts(lat=0, lon=0):
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():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(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:
wxAlertCache = ""
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
@@ -400,7 +435,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]
@@ -415,3 +450,113 @@ 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.warning(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

View File

@@ -1,4 +1,5 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from modules.settings import *
@@ -63,14 +64,32 @@ logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler_sys = logging.FileHandler('logs/meshbot{}.log'.format(today.strftime('%Y_%m_%d')))
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.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('logs/messages{}.log'.format(today.strftime('%Y_%m_%d')))
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
@@ -19,11 +20,11 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
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
@@ -40,38 +41,53 @@ 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'}
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'))
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')
@@ -89,7 +105,7 @@ if 'interface2' in config:
else:
interface2_enabled = False
# variables
# variables from the config.ini file
try:
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
@@ -97,6 +113,7 @@ try:
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
store_forward_enabled = config['general'].getboolean('StoreForward', True)
@@ -105,22 +122,31 @@ 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)
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
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
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)
@@ -132,17 +158,54 @@ try:
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
mySAME = config['location'].get('mySAME', '').split(',') # default empty
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
femaAlertBroadcastEnabled = config['location'].getboolean('femaAlertBroadcastEnabled', False) # default False
femaAlertBroadcastCh = config['location'].get('femaAlertBroadcastCh', '2').split(',') # default Channel 2
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if wxAlertBroadcastChannel:
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
# 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
@@ -151,7 +214,14 @@ try:
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
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
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
dopewars_enabled = config['games'].getboolean('dopeWars', True)
@@ -160,12 +230,13 @@ try:
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
mastermind_enabled = config['games'].getboolean('mastermind', True)
golfSim_enabled = config['games'].getboolean('golfSim', True)
uno_enabled = config['games'].getboolean('uno', True)
# messaging settings
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
except KeyError as e:
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", "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

View File

@@ -6,16 +6,17 @@ import meshtastic.tcp_interface
import meshtastic.ble_interface
import time
import asyncio
import random
import contextlib # for suppressing output on watchdog
import io # for suppressing output on watchdog
from modules.log import *
# Global Variables
trap_list = ("cmd","cmd?") # default trap list
help_message = "Bot CMD?:\n"
help_message = "Bot CMD?:"
asyncLoop = asyncio.new_event_loop()
games_enabled = False
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0}]
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
# Ping Configuration
@@ -37,17 +38,30 @@ if motd_enabled:
trap_list = trap_list + trap_list_motd
help_message = help_message + ", motd"
# SMTP Configuration
if enableSMTP:
from modules.smtp import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_smtp
help_message = help_message + ", email:, sms:"
# Emergency Responder Configuration
if emergency_responder_enabled:
trap_list_emergency = ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue")
trap_list = trap_list + trap_list_emergency
# whoami Configuration
if whoami_enabled:
trap_list_whoami = ("whoami", "📍")
trap_list_whoami = ("whoami", "📍", "whois")
trap_list = trap_list + trap_list_whoami
help_message = help_message + ", whoami"
# Solar Conditions Configuration
if solar_conditions_enabled:
from modules.solarconditions import * # from the spudgunman/meshing-around repo
from modules.space import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
help_message = help_message + ", sun, hfcond, solar, moon"
if n2yoAPIKey != "":
help_message = help_message + ", satpass"
else:
hf_band_conditions = False
@@ -67,7 +81,7 @@ if location_enabled:
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
else:
# NOAA only features
help_message = help_message + ", wxa, tide"
help_message = help_message + ", wxa, tide, ealert"
# BBS Configuration
if bbs_enabled:
@@ -129,11 +143,6 @@ if golfSim_enabled:
from modules.games.golfsim import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("golfsim",)
games_enabled = True
if uno_enabled:
from modules.games.uno import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("playuno",)
games_enabled = True
# Games Configuration
if games_enabled is True:
@@ -155,8 +164,6 @@ if games_enabled is True:
gamesCmdList += "masterMind, "
if golfSim_enabled:
gamesCmdList += "golfSim, "
if uno_enabled:
gamesCmdList += "playuno, "
gamesCmdList = gamesCmdList[:-2] # remove the last comma
else:
gamesCmdList = ""
@@ -164,8 +171,6 @@ else:
# Scheduled Broadcast Configuration
if scheduler_enabled:
import schedule # pip install schedule
# 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"))
# Sentry Configuration
if sentry_enabled:
@@ -181,6 +186,21 @@ if store_forward_enabled:
if radio_detection_enabled:
from modules.radio import * # from the spudgunman/meshing-around repo
# File Monitor Configuration
if file_monitor_enabled or read_news_enabled:
from modules.filemon import * # from the spudgunman/meshing-around repo
if read_news_enabled:
trap_list = trap_list + trap_list_filemon # items readnews
help_message = help_message + ", readmail"
# clean up the help message
help_message = help_message.split(", ")
help_message.sort()
if len(help_message) > 20:
# split in half for formatting
help_message = help_message[:len(help_message)//2] + ["\nCMD?"] + help_message[len(help_message)//2:]
help_message = ", ".join(help_message)
# BLE dual interface prevention
if interface1_type == 'ble' and interface2_type == 'ble':
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
@@ -428,22 +448,39 @@ def messageChunker(message):
else:
# split the part into chunks
current_chunk = ''
sentences = part.split('. ')
sentences = []
sentence = ''
for char in part:
sentence += char
if char in '.!?':
sentences.append(sentence.strip())
sentence = ''
if sentence:
sentences.append(sentence.strip())
for sentence in sentences:
sentence = sentence.strip()
sentence = sentence.replace(' ', ' ')
# remove empty sentences
if not sentence:
continue
# remove junk sentences and append to the previous sentence this may exceed the MESSAGE_CHUNK_SIZE by 3
if len(sentence) < 4:
if current_chunk:
current_chunk += sentence
else:
current_chunk = sentence
continue
# if sentence is too long, split it by words
if len(current_chunk) + len(sentence) > MESSAGE_CHUNK_SIZE:
message_list.append(current_chunk)
if current_chunk:
message_list.append(current_chunk)
current_chunk = sentence
else:
if current_chunk:
current_chunk += ' '
current_chunk += sentence
current_chunk += ' ' + sentence
else:
current_chunk = sentence
if current_chunk:
message_list.append(current_chunk)
@@ -464,12 +501,18 @@ def messageChunker(message):
return message
def send_message(message, ch, nodeid=0, nodeInt=1):
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
# Send a message to a channel or DM
interface = interface1 if nodeInt == 1 else interface2
# Check if the message is empty
if message == "" or message == None or len(message) == 0:
return False
interface = interface1 if nodeInt == 1 else interface2
# Split the message into chunks if it exceeds the MESSAGE_CHUNK_SIZE
message_list = messageChunker(message)
if not bypassChuncking:
# Split the message into chunks if it exceeds the MESSAGE_CHUNK_SIZE
message_list = messageChunker(message)
else:
message_list = [message]
if isinstance(message_list, list):
# Send the message to the channel or DM
@@ -479,13 +522,22 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
chunkOf = f"{message_list.index(m)+1}/{num_chunks}"
if nodeid == 0:
# Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
interface.sendText(text=m, channelIndex=ch)
if wantAck:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
interface.sendText(text=m, channelIndex=ch, wantAck=True)
else:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
interface.sendText(text=m, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
if wantAck:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
else:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
# Throttle the message sending to prevent spamming the device
if (message_list.index(m)+1) % 4 == 0:
@@ -499,13 +551,22 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0:
# Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
interface.sendText(text=message, channelIndex=ch)
if wantAck:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "req.ACK " + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
interface.sendText(text=message, channelIndex=ch, wantAck=True)
else:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
interface.sendText(text=message, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
if wantAck:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "req.ACK " + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
else:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
return True
def get_wikipedia_summary(search_term):
@@ -533,23 +594,6 @@ def get_wikipedia_summary(search_term):
return summary
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
def messageTrap(msg):
# Check if the message contains a trap word, this is the first filter for listning to messages
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
@@ -578,13 +622,38 @@ def handleMultiPing(nodeID=0, deviceID=1):
count = mPlCpy[i]['count']
type = mPlCpy[i]['type']
deviceID = mPlCpy[i]['deviceID']
channel_number = mPlCpy[i]['channel_number']
start_count = mPlCpy[i]['startCount']
if count > 1 and deviceID == 1:
if count > 1:
count -= 1
# update count in the list
multiPingList[i]['count'] = count
for i in range(len(multiPingList)):
if multiPingList[i]['message_from_id'] == message_id_from:
multiPingList[i]['count'] = count
send_message(f"🔂{count} {type}", publicChannel, message_id_from, 1)
# handle bufferTest
if type == '🎙TEST':
buffer = ''.join(random.choice(['0', '1']) for i in range(maxBuffer))
# divide buffer by start_count and get resolution
resolution = maxBuffer // start_count
slice = resolution * count
if slice > maxBuffer:
slice = maxBuffer
# set the type as a portion of the buffer
type = buffer[slice - resolution:]
# if exceed the maxBuffer, remove the excess
count = len(type + "🔂 ")
if count > maxBuffer:
type = type[:maxBuffer - count]
# final length count of the message for display
count = len(type + "🔂 ")
if count < 99:
count -= 1
# send the DM
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID, bypassChuncking=True)
time.sleep(responseDelay + 1)
if count < 2:
# remove the item from the list
for j in range(len(multiPingList)):
@@ -592,6 +661,27 @@ def handleMultiPing(nodeID=0, deviceID=1):
multiPingList.pop(j)
break
def handleWxBroadcast(deviceID=1):
# only allow API call every 20 minutes
# the watchdog will call this function 3 times, seeing possible throttling on the API
clock = datetime.now()
if clock.minute % 20 != 0:
return False
if clock.second > 17:
return False
# check for alerts
alert = alertBrodcast()
if alert:
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
if isinstance(wxAlertBroadcastChannel, list):
for channel in wxAlertBroadcastChannel:
send_message(msg, int(channel), 0, deviceID)
else:
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
return True
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
@@ -820,7 +910,7 @@ async def BroadcastScheduler():
await asyncio.sleep(1)
async def handleSignalWatcher():
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
global lastHamLibAlert
# monitor rigctld for signal strength and frequency
while True:
msg = await signalWatcher()
@@ -850,6 +940,36 @@ async def handleSignalWatcher():
await asyncio.sleep(1)
pass
async def handleFileWatcher():
global lastFileAlert
# monitor the file system for changes
while True:
msg = await watch_file()
if msg != ERROR_FETCHING_DATA and msg is not None:
logger.debug(f"System: Detected Alert from FileWatcher on file {file_monitor_file_path}")
# check we are not spammig the channel limit messages to once per minute
if time.time() - lastFileAlert > 60:
lastFileAlert = time.time()
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
if type(file_monitor_broadcastCh) is list:
for ch in file_monitor_broadcastCh:
if antiSpam and ch != publicChannel:
send_message(msg, int(ch), 0, 1)
if interface2_enabled:
send_message(msg, int(ch), 0, 2)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
else:
if antiSpam and file_monitor_broadcastCh != publicChannel:
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
if interface2_enabled:
send_message(msg, int(file_monitor_broadcastCh), 0, 2)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
await asyncio.sleep(1)
pass
async def retry_interface(nodeID=1):
global interface1, interface2, retry_int1, retry_int2, max_retry_count1, max_retry_count2
@@ -897,7 +1017,6 @@ async def retry_interface(nodeID=1):
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
handleSentinel_spotted = ""
handleSentinel_loop = 0
async def handleSentinel(deviceID=1):
@@ -905,6 +1024,7 @@ async def handleSentinel(deviceID=1):
# Locate Closest Nodes and report them to a secure channel
# async function for possibly demanding back location data
enemySpotted = ""
resolution = "unknown"
closest_nodes = get_closest_nodes(deviceID)
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
if closest_nodes[0]['id'] is not None:
@@ -916,12 +1036,16 @@ async def handleSentinel(deviceID=1):
if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted:
# check the positionMetadata for nodeID and get metadata
if positionMetadata and closest_nodes[0]['id'] in positionMetadata:
if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata:
metadata = positionMetadata[closest_nodes[0]['id']]
resolution = metadata.get('precisionBits', 'na')
if metadata.get('precisionBits') is not None:
resolution = metadata.get('precisionBits')
logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits")
send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID)
if enableSMTP and email_sentry_alerts:
for email in sysopEmails:
send_email(email, f"Sentry{deviceID}: {enemySpotted}")
handleSentinel_loop = 0
handleSentinel_spotted = enemySpotted
else:
@@ -950,6 +1074,9 @@ async def watchdog():
# multiPing handler
handleMultiPing(0,1)
if wxAlertBroadcastEnabled:
handleWxBroadcast(1)
# Telemetry data
int1Data = displayNodeTelemetry(0, 1)
if int1Data != -1 and telemetryData[0]['lastAlert1'] != int1Data:
@@ -979,6 +1106,9 @@ async def watchdog():
# multiPing handler
handleMultiPing(0,2)
if wxAlertBroadcastEnabled:
handleWxBroadcast(2)
# Telemetry data
int2Data = displayNodeTelemetry(0, 2)
if int2Data != -1 and telemetryData[0]['lastAlert2'] != int2Data:
@@ -991,4 +1121,3 @@ async def watchdog():
except Exception as e:
logger.error(f"System: retrying interface2: {e}")

1
news.txt Normal file
View File

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

View File

@@ -8,24 +8,29 @@ from pubsub import pub # pip install pubsub
from modules.log import *
from modules.system import *
# Global Variables
cmdHistory = [] # list to hold the last commands
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),
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: help_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),
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"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: 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),
"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 +49,88 @@ 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"
# disabled in channel
if autoPingInChannel and not isDM:
# 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
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"
else:
return "🏓PONG, " + hop
msg = "🔊AutoPing via DM only⛔"
# if not a DM add the username to the beginning of msg
if not isDM:
msg = get_name_from_number(message_from_id) + msg
return msg
def handle_motd(message):
global MOTD
@@ -65,64 +141,37 @@ def handle_motd(message):
else:
return MOTD
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
# 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')
isDM = False
if DEBUGpacket:
# Debug print the interface object
for item in interface.__dict__.items(): intDebug = f"{item}\n"
@@ -130,6 +179,7 @@ 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:
@@ -150,42 +200,47 @@ def onReceive(packet, interface):
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# check for a message packet and process it
# 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
# 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 +251,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
@@ -269,10 +331,12 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
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)
@@ -286,16 +350,32 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
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")
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
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 +386,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,7 +3,6 @@ pubsub
datetime
pyephem
requests
geopy
maidenhead
beautifulsoup4
dadjokes
@@ -13,7 +12,4 @@ numpy
geopy
schedule
wikipedia
ollama
langchain
langchain-ollama
googlesearch-python