Compare commits

...

1197 Commits

Author SHA1 Message Date
SpudGunMan
1324f83f17 enhance schedule
allow basic Joke and Weather messages with out extra config
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
2025-10-20 12:07:22 -07:00
SpudGunMan
08ae8c31a0 enhance:fix
I added some things to help any future aarg, sorry I broke it again I also
2025-10-20 11:45:49 -07:00
SpudGunMan
957e803951 cleanup 2025-10-20 11:35:26 -07:00
SpudGunMan
2de3441d67 enhance
with time stamp and also better CSV answers for review
2025-10-20 09:54:44 -07:00
SpudGunMan
2b420022f9 timeFix
will be make year 2000
2025-10-20 09:54:25 -07:00
SpudGunMan
d01f143adf Update system.py 2025-10-20 09:24:18 -07:00
SpudGunMan
5f5aeeadac Update system.py 2025-10-20 09:22:45 -07:00
SpudGunMan
d57826613c moved
to slurp repo
2025-10-20 09:19:18 -07:00
SpudGunMan
af09dc0cf9 improveUse 2025-10-19 22:43:51 -07:00
SpudGunMan
011bac41f2 lower 2025-10-19 22:32:06 -07:00
SpudGunMan
20467ea886 enhance 2025-10-19 22:30:40 -07:00
SpudGunMan
bbfd71f011 "IP-Network" 2025-10-19 22:27:40 -07:00
SpudGunMan
e1ff87a197 enhance Ping 2025-10-19 22:26:09 -07:00
SpudGunMan
a859f830bb coreFix
Enhance Packet hop and MQTT detection
2025-10-19 22:16:19 -07:00
SpudGunMan
d99698e7f3 Update blackjack.py 2025-10-19 20:25:36 -07:00
SpudGunMan
5ecc563e96 newChunker 2025-10-19 20:18:08 -07:00
SpudGunMan
eeeb43cacc Update lemonade.py 2025-10-19 20:12:28 -07:00
SpudGunMan
9fdcea56fc Update lemonade.py 2025-10-19 20:10:26 -07:00
SpudGunMan
24a33fe882 Update lemonade.py 2025-10-19 20:08:34 -07:00
SpudGunMan
5710cebf39 Update mmind.py 2025-10-19 20:04:05 -07:00
SpudGunMan
b66487863d Update mmind.py 2025-10-19 19:35:41 -07:00
SpudGunMan
b3c4d208b7 Update mmind.py 2025-10-19 19:29:34 -07:00
SpudGunMan
f41ff2d5f7 Update mmind.py 2025-10-19 19:21:48 -07:00
SpudGunMan
48366bc595 Update mmind.py 2025-10-19 19:15:31 -07:00
SpudGunMan
02dd64382d Update mesh_bot.py 2025-10-19 19:05:23 -07:00
SpudGunMan
731b48ad65 Update mesh_bot.py 2025-10-19 19:04:34 -07:00
SpudGunMan
69a7082669 Update README.md 2025-10-19 18:57:56 -07:00
SpudGunMan
fafa7d8a51 Update config.template 2025-10-19 18:57:27 -07:00
Kelly
6e69b5f014 Merge pull request #219 from pdxlocations/allow-port-numbers
Enhance TCP interface initialization to support host:port format
2025-10-19 18:54:54 -07:00
SpudGunMan
03895248cd fixing
sorry if you saw the crashing I had dinner
2025-10-19 18:31:51 -07:00
SpudGunMan
a79de8a325 cleanupBadCode 2025-10-19 17:16:53 -07:00
SpudGunMan
740b53f02f Update system.py 2025-10-19 16:25:35 -07:00
SpudGunMan
76e75551c6 Update videopoker.py 2025-10-19 16:15:49 -07:00
SpudGunMan
51752ae896 Update videopoker.py 2025-10-19 16:15:37 -07:00
SpudGunMan
d81e773c0c Update blackjack.py 2025-10-19 16:02:24 -07:00
SpudGunMan
1f1ed1ca70 Update blackjack.py 2025-10-19 16:01:04 -07:00
pdxlocations
df9f3806a3 Enhance TCP interface initialization to support host:port format 2025-10-19 16:00:45 -07:00
SpudGunMan
081ccd9e2e Update blackjack.py 2025-10-19 15:55:46 -07:00
SpudGunMan
d9a7dafe6e Update blackjack.py 2025-10-19 15:54:41 -07:00
SpudGunMan
921225965b Update blackjack.py 2025-10-19 15:52:20 -07:00
SpudGunMan
3659254785 refactor suggestion 2025-10-19 15:42:49 -07:00
SpudGunMan
7c502608f6 remove deps install 2025-10-19 14:51:51 -07:00
SpudGunMan
427c25f80b noHoldsHeld 2025-10-19 14:26:45 -07:00
SpudGunMan
c3f15390ea Update system.py 2025-10-19 14:08:21 -07:00
SpudGunMan
e1476a44c6 enhance 2025-10-19 13:46:58 -07:00
SpudGunMan
72070fef3e backup data 2025-10-19 13:43:57 -07:00
SpudGunMan
b63ea677f6 Update blackjack.py 2025-10-19 13:35:00 -07:00
SpudGunMan
f8389500b8 Update mesh_bot.py 2025-10-19 13:28:32 -07:00
SpudGunMan
b257625a45 cleanupBlackJack 2025-10-19 13:23:40 -07:00
SpudGunMan
a233d8c7b3 Update mesh_bot.py 2025-10-19 12:57:37 -07:00
SpudGunMan
11c9742ebe cleanup 2025-10-19 12:55:16 -07:00
SpudGunMan
5af28c3dc2 Update system.py
kidding
2025-10-19 12:55:08 -07:00
SpudGunMan
aebb9e3c20 cleanup 2025-10-19 12:48:19 -07:00
SpudGunMan
d5916f4ccc 🐞🧩
thanks meshguy
2025-10-19 12:22:09 -07:00
SpudGunMan
056159a3f3 Update mesh_bot.py 2025-10-19 09:21:25 -07:00
SpudGunMan
2f6049d94b bugfix survey game 2025-10-18 19:20:36 -07:00
SpudGunMan
a2d7f664ab Update udp.py 2025-10-18 17:29:21 -07:00
SpudGunMan
b26491b646 Update udp.py 2025-10-18 17:26:39 -07:00
SpudGunMan
22e97b0eec Update udp.py 2025-10-18 16:54:26 -07:00
SpudGunMan
f540866d08 Update locationdata.py 2025-10-18 15:18:18 -07:00
SpudGunMan
c9729c8214 Update locationdata.py 2025-10-18 15:17:34 -07:00
SpudGunMan
49901cbbee Update locationdata.py 2025-10-18 15:14:23 -07:00
SpudGunMan
2aa2b80935 Update locationdata.py 2025-10-18 15:08:40 -07:00
SpudGunMan
95695f4f58 Update locationdata.py 2025-10-18 15:02:33 -07:00
SpudGunMan
b641d2b5e8 ok finslly
this looks better
2025-10-18 14:54:45 -07:00
SpudGunMan
51d8faab12 enhance 2025-10-18 14:49:26 -07:00
SpudGunMan
7a1396b99d Update locationdata.py 2025-10-18 14:40:39 -07:00
SpudGunMan
819bbbcaf4 enhance 2025-10-18 14:39:06 -07:00
SpudGunMan
0eeda96670 Update locationdata.py 2025-10-18 14:36:44 -07:00
SpudGunMan
18cca4ffdd Update locationdata.py 2025-10-18 14:35:28 -07:00
SpudGunMan
d169fe2dff Update locationdata.py 2025-10-18 14:33:42 -07:00
SpudGunMan
1c732dfe17 Update install.sh 2025-10-18 12:47:17 -07:00
SpudGunMan
bdad3927e5 enhance 2025-10-18 10:07:09 -07:00
SpudGunMan
0e0d6416d9 enhance config merge data 2025-10-18 09:38:00 -07:00
SpudGunMan
0da780371a enhance 2025-10-18 09:10:47 -07:00
SpudGunMan
37bf30cbc0 enhance 2025-10-18 09:05:17 -07:00
SpudGunMan
817a8601dd Update system.py 2025-10-18 08:53:30 -07:00
SpudGunMan
47cca409be lab work 2025-10-18 08:52:32 -07:00
SpudGunMan
e08a82ec39 Update system.py 2025-10-18 08:42:48 -07:00
SpudGunMan
345541dfb5 Update system.py 2025-10-18 08:41:22 -07:00
SpudGunMan
6e89762f1d bbsCompression
not enabled yet
2025-10-18 08:39:43 -07:00
SpudGunMan
0fb26bc16a Update mesh_bot.py 2025-10-17 19:50:47 -07:00
SpudGunMan
f1ad5966af send_raw_bytes 2025-10-17 19:50:41 -07:00
SpudGunMan
ac57d4683f Update udp.py 2025-10-17 17:48:24 -07:00
SpudGunMan
eab099e5ee channelID 2025-10-17 17:42:07 -07:00
SpudGunMan
685bd3491d Update udp.py 2025-10-17 17:10:44 -07:00
SpudGunMan
b8d64f3a9e Update system.py 2025-10-17 13:31:47 -07:00
SpudGunMan
852d491030 Update meshview.ino 2025-10-16 18:57:17 -07:00
SpudGunMan
76565c5546 Update meshview.ino 2025-10-16 18:55:03 -07:00
SpudGunMan
af1ec1630e Update udp.py 2025-10-16 16:04:06 -07:00
SpudGunMan
0c2b36a206 refactor handle_messages
@mesb1 give this one a test

https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-16 15:55:12 -07:00
SpudGunMan
c0934096f0 Update meshview.ino 2025-10-16 12:07:58 -07:00
SpudGunMan
819bfaba90 Update meshview.ino 2025-10-16 11:52:03 -07:00
SpudGunMan
8041a1296b tinkering
@martinbogo
2025-10-15 20:32:46 -07:00
SpudGunMan
10d93b4fd3 keyFactor 2025-10-15 20:17:44 -07:00
SpudGunMan
19dedef1e6 meshview.ino
I could use help with this I am stuck at the moment
2025-10-15 19:25:21 -07:00
SpudGunMan
d4af0c7e8b Update udp.py 2025-10-15 15:58:02 -07:00
SpudGunMan
8730f0fd38 Update udp.py 2025-10-15 15:57:40 -07:00
SpudGunMan
9cda8daf65 Update udp.py 2025-10-15 15:57:24 -07:00
SpudGunMan
a9223f1613 Create udp.py 2025-10-15 15:51:30 -07:00
SpudGunMan
04ca4c99b8 Update scheduler.py
sorry for that
2025-10-15 08:24:02 -07:00
SpudGunMan
3072520e63 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-10-15 08:23:16 -07:00
SpudGunMan
bd6603766b Update scheduler.py 2025-10-15 08:23:14 -07:00
Kelly
075a23bd2b LowerBits
https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 22:21:21 -07:00
SpudGunMan
a8e4f653ed Update radio.py 2025-10-14 21:19:00 -07:00
SpudGunMan
374a44f4a9 Update radio.py 2025-10-14 21:17:36 -07:00
SpudGunMan
3c8d2e646e Update radio.py 2025-10-14 16:32:01 -07:00
SpudGunMan
e5df983244 Update mesh_bot.py 2025-10-14 16:23:27 -07:00
SpudGunMan
fa5f9250c4 Update llm.py 2025-10-14 16:14:59 -07:00
SpudGunMan
3f7a831690 Update llm.py 2025-10-14 16:12:14 -07:00
SpudGunMan
89aaaddae9 Update llm.py 2025-10-14 16:02:40 -07:00
SpudGunMan
e1919616c2 refactoring 2025-10-14 15:55:59 -07:00
SpudGunMan
8b9e637006 Update README.md 2025-10-14 15:08:07 -07:00
SpudGunMan
0df3e32901 Update README.md 2025-10-14 15:07:18 -07:00
SpudGunMan
1c2fa174ea Hey Chirpy
- **Voice/Command Triggers**: The following keywords can be used in messages or via voice (VOX) to trigger bot functions:
  - `joke`: Tells a joke
  - `weather`: Returns local weather forecast
  - `moon`: Returns moonrise/set and phase info
  - `daylight`: Returns sunrise/sunset times
  - `river`: Returns NOAA river flow info
  - `tide`: Returns NOAA tide information
  - `satellite`: Returns satellite pass info
2025-10-14 15:05:39 -07:00
SpudGunMan
c97aefcef1 Update radio.py 2025-10-14 14:59:10 -07:00
SpudGunMan
dfb94c3993 voxUse 2025-10-14 14:56:45 -07:00
SpudGunMan
7d62f69f12 ... 2025-10-14 14:24:42 -07:00
SpudGunMan
cf896767fb Update mesh_bot.py 2025-10-14 13:41:31 -07:00
SpudGunMan
1eb4cf71ed Update mesh_bot.py 2025-10-14 13:40:27 -07:00
SpudGunMan
e959124eac voxEnhance 2025-10-14 13:38:26 -07:00
SpudGunMan
d787c72812 Update radio.py 2025-10-14 13:32:22 -07:00
SpudGunMan
9f0dd56d43 Update mesh_bot.py 2025-10-14 13:26:37 -07:00
SpudGunMan
aa71e6045a Update radio.py
forgot to save a good
2025-10-14 12:45:41 -07:00
SpudGunMan
a140ad83cd Update radio.py 2025-10-14 12:34:44 -07:00
SpudGunMan
93c2d731e8 Update radio.py 2025-10-14 12:33:18 -07:00
SpudGunMan
d8da553af9 Update radio.py 2025-10-14 12:32:21 -07:00
SpudGunMan
9d9f070908 enhance 2025-10-14 12:24:42 -07:00
SpudGunMan
0f2061af55 chirpy make my lunch 2025-10-14 12:24:29 -07:00
SpudGunMan
d8423584d4 Update radio.py 2025-10-14 12:19:13 -07:00
SpudGunMan
843320d268 Update radio.py 2025-10-14 12:18:11 -07:00
SpudGunMan
216128b15a Update radio.py 2025-10-14 12:07:45 -07:00
SpudGunMan
f8bc574753 Update radio.py 2025-10-14 11:35:32 -07:00
SpudGunMan
6193c5933f Update radio.py 2025-10-14 11:32:34 -07:00
SpudGunMan
b668965bda bufferHandler
tracking https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 11:13:40 -07:00
SpudGunMan
ae039b5baf Update radio.py 2025-10-14 11:01:24 -07:00
SpudGunMan
824d43f16e Update radio.py 2025-10-14 10:57:39 -07:00
SpudGunMan
2de76e6c5e Update radio.py 2025-10-14 10:56:14 -07:00
SpudGunMan
afb02602fd Update radio.py 2025-10-14 10:53:44 -07:00
SpudGunMan
99528c2bcf Update system.py 2025-10-14 10:52:35 -07:00
SpudGunMan
b53f5821f3 Update system.py 2025-10-14 10:51:34 -07:00
SpudGunMan
93fc6547b8 Update system.py 2025-10-14 10:46:30 -07:00
SpudGunMan
9a7e321dff Update scheduler.py 2025-10-14 10:25:32 -07:00
SpudGunMan
39257f2d39 Update scheduler.py 2025-10-14 10:25:16 -07:00
SpudGunMan
8c5abecac3 refactor
or custom for module/scheduler.py
2025-10-14 10:09:33 -07:00
SpudGunMan
16dcc96037 consolidate time to wait 2025-10-14 09:38:38 -07:00
SpudGunMan
b1d32a7745 refactor MOTD 2025-10-14 09:22:41 -07:00
SpudGunMan
631a2f53ea major refactor to schedule
major thanks to @FJRPiolt
2025-10-14 08:44:59 -07:00
SpudGunMan
32903c97e3 enhance 2025-10-14 08:13:35 -07:00
SpudGunMan
6e61e8122d Update system.py
no its not
2025-10-14 08:07:00 -07:00
SpudGunMan
d109803f9d Update system.py 2025-10-14 07:21:05 -07:00
SpudGunMan
09ed4f57cf Update system.py
enhance high fly with block list blocks
2025-10-14 07:18:10 -07:00
SpudGunMan
acfb8078a9 Update system.py
to much log
2025-10-14 07:06:46 -07:00
SpudGunMan
84f9693833 Update system.py 2025-10-13 23:50:32 -07:00
SpudGunMan
50fdcf486d Update system.py 2025-10-13 23:21:50 -07:00
SpudGunMan
eab5afccc8 Update system.py
helps to hit save
2025-10-13 21:31:10 -07:00
SpudGunMan
ea9db47c2d refactor sysinfo local telemetry 2025-10-13 21:29:45 -07:00
SpudGunMan
cf3a9c5b43 Update filemon.py 2025-10-13 19:49:43 -07:00
SpudGunMan
adedaa092c Update mesh_bot.py
fixLocaStats and sysinfo
2025-10-13 19:49:24 -07:00
SpudGunMan
f204237a63 Update mesh_bot.py 2025-10-13 19:27:58 -07:00
SpudGunMan
057a400041 Update mesh_bot.py 2025-10-13 19:26:53 -07:00
SpudGunMan
4cdf68f074 fixLocaStats and sysinfo 2025-10-13 19:24:37 -07:00
SpudGunMan
003a11c557 fixReportingEngine
This data is used by the webReporting engine
2025-10-13 17:57:20 -07:00
SpudGunMan
8d309fa579 Update README.md 2025-10-13 17:42:35 -07:00
SpudGunMan
232f9c24db aaahhhrrg 2025-10-13 17:27:51 -07:00
SpudGunMan
39dccd149b Update mesh_bot.py 2025-10-13 17:26:45 -07:00
SpudGunMan
b921c73fa7 Update mesh_bot.py 2025-10-13 17:26:08 -07:00
SpudGunMan
f3ec1cbe93 enhance 2025-10-13 17:23:49 -07:00
SpudGunMan
a6bcfda0ac enhance 2025-10-13 17:20:56 -07:00
SpudGunMan
51cd2002af Update system.py 2025-10-13 17:13:37 -07:00
SpudGunMan
b40f41f41c bannode
bad node! this isnt saving to .ini
2025-10-13 17:12:27 -07:00
SpudGunMan
4c33b30f14 addMessageData
Co-Authored-By: Martin Bogomolni <martinbogo@igotu.com>
2025-10-13 15:22:29 -07:00
SpudGunMan
b7490afb99 Update llm.py 2025-10-13 15:03:42 -07:00
SpudGunMan
8b57ed727c Update mesh_bot.py 2025-10-13 13:50:07 -07:00
SpudGunMan
fd5d64b9fb 🫖
enhance
2025-10-13 13:14:32 -07:00
SpudGunMan
00af152c2c Update system.py
slowing this a bit
2025-10-13 12:28:41 -07:00
SpudGunMan
31f0abc8c8 requestPosition
alsoRequesting feedback if this works well? you will need to edit the file find the `reqLocationEnabled` and set True. save and test it out
2025-10-13 12:00:36 -07:00
SpudGunMan
6b7d795a31 Update README.md 2025-10-13 10:04:13 -07:00
SpudGunMan
1f093c4bc2 Update system.py 2025-10-13 10:02:22 -07:00
SpudGunMan
fe1c4a1ad0 Update locationdata.py 2025-10-13 10:02:20 -07:00
SpudGunMan
11687cb7ba ‼️UPDATE LOCATION🗺️
this is a fail safe change to fuzzing the default location. This may change the way you use the bot today and should evaluate the change specifically test the auto alerts for proper data for emergency alerts etc.`fuzzConfigLocation = True`
2025-10-13 09:49:10 -07:00
SpudGunMan
b07a7fb0cc Update radio.py 2025-10-13 08:40:21 -07:00
SpudGunMan
b876d87ba9 enhance 2025-10-13 08:38:27 -07:00
SpudGunMan
0a63e89633 waitTooLong!
haha I well sorry
2025-10-13 08:23:07 -07:00
SpudGunMan
848f5609c2 Update README.md 2025-10-12 23:22:33 -07:00
SpudGunMan
0ccbed6165 fix Lemons 2025-10-12 23:19:08 -07:00
SpudGunMan
646517db71 Update mesh_bot.py 2025-10-12 21:27:14 -07:00
SpudGunMan
7d347bb80a enhance 2025-10-12 21:24:58 -07:00
SpudGunMan
e199d4f5eb Update mesh_bot.py 2025-10-12 20:03:03 -07:00
SpudGunMan
a9767b58c4 Update mesh_bot.py 2025-10-12 20:00:22 -07:00
SpudGunMan
69dfde047e Update lemonade.py 2025-10-12 20:00:20 -07:00
SpudGunMan
da33b6f1b9 Update dopewar.py 2025-10-12 19:55:43 -07:00
SpudGunMan
8a7125358b Update lemonade.py 2025-10-12 18:23:14 -07:00
SpudGunMan
ae558052f7 hey chirpy
vox trapping
2025-10-12 18:17:05 -07:00
SpudGunMan
5074d71eb7 defaults 2025-10-12 17:22:02 -07:00
SpudGunMan
632f42477a Update settings.py 2025-10-12 17:18:44 -07:00
SpudGunMan
b3df38d15e Update radio.py
aaarg
2025-10-12 17:17:31 -07:00
SpudGunMan
b76b8ca718 Update radio.py 2025-10-12 17:17:02 -07:00
SpudGunMan
d66a9e745b enhance 2025-10-12 17:13:41 -07:00
SpudGunMan
717bbccea3 Omg 2025-10-12 16:25:43 -07:00
SpudGunMan
50fd1c0410 Update tictactoe.py 2025-10-12 16:22:25 -07:00
SpudGunMan
ae89788ea4 Update settings.py 2025-10-12 15:33:02 -07:00
SpudGunMan
4220b095ee Update addFav.py 2025-10-12 15:27:30 -07:00
SpudGunMan
ef28341cdb Update addFav.py 2025-10-12 15:07:27 -07:00
SpudGunMan
b5d610728c Update addFav.py
ffs
2025-10-12 15:03:44 -07:00
SpudGunMan
bc238ef476 Update addFav.py 2025-10-12 14:38:20 -07:00
SpudGunMan
feb3544014 fixBug 2025-10-12 14:32:59 -07:00
SpudGunMan
31322dc0cd lessWait
remove some waits now that 2 seconds is needed by firmware
2025-10-12 14:26:23 -07:00
SpudGunMan
8c1cbaf442 fix game bugs 2025-10-12 13:05:54 -07:00
SpudGunMan
8d6a95b5da OMG
sorry to the fans
2025-10-12 11:00:09 -07:00
SpudGunMan
b4b2ef3d80 Update system.py
expand the timers for game play to 3 days for cleanup
2025-10-12 10:05:30 -07:00
SpudGunMan
13b1b90864 Update system.py 2025-10-12 10:00:09 -07:00
SpudGunMan
838bd3edce Update radio.py
oops
2025-10-12 09:55:55 -07:00
SpudGunMan
70bcf43b49 Update radio.py 2025-10-12 09:54:56 -07:00
SpudGunMan
0a02ae860e Update radio.py 2025-10-11 22:21:09 -07:00
SpudGunMan
a80575a381 Update radio.py 2025-10-11 22:20:52 -07:00
SpudGunMan
e57b65b447 Update system.py 2025-10-11 21:58:39 -07:00
SpudGunMan
1d18f0936c Update radio.py 2025-10-11 21:25:52 -07:00
SpudGunMan
b096716b96 enhance import note 2025-10-11 21:19:26 -07:00
SpudGunMan
f4734c5b87 vox detection 2025-10-11 20:44:03 -07:00
SpudGunMan
96447b166f Update README.md 2025-10-11 14:04:41 -07:00
SpudGunMan
2fc151bbbf leaderboard
I hope its all working now!
2025-10-11 12:55:56 -07:00
SpudGunMan
e66af5c068 bug in leaderboard fix 2025-10-11 12:47:24 -07:00
SpudGunMan
d3ce4d3905 Update addFav.py 2025-10-11 12:38:20 -07:00
SpudGunMan
a95cdeb086 -pickle
or print either way
2025-10-11 12:35:58 -07:00
SpudGunMan
27bf61a913 Update addFav.py 2025-10-11 12:21:00 -07:00
SpudGunMan
d62990b6db Update system.py
seriously open a window
2025-10-11 12:10:03 -07:00
SpudGunMan
0784aaebd9 enhance 2025-10-11 12:09:18 -07:00
SpudGunMan
e348854a50 cleanup 2025-10-11 11:46:09 -07:00
SpudGunMan
a71e5fa8f3 Update addFav.py 2025-10-11 11:22:21 -07:00
SpudGunMan
9600ea5e00 enhance with client_base 2025-10-11 11:20:00 -07:00
SpudGunMan
cc7461929e cleanup 2025-10-11 11:19:52 -07:00
SpudGunMan
0c8fb0c243 cleanup 2025-10-11 07:54:20 -07:00
SpudGunMan
311563320e enhance
rss sucks
2025-10-10 19:41:44 -07:00
SpudGunMan
78b6d660dd yakima works 2025-10-10 16:37:30 -07:00
SpudGunMan
77da966b9d Update mesh_bot.py 2025-10-10 16:22:59 -07:00
SpudGunMan
d844c123be refactor Rivers 2025-10-10 16:02:23 -07:00
SpudGunMan
d4d36c8a31 increase urlTimeout to 15 seconds 2025-10-10 15:30:36 -07:00
SpudGunMan
9acd57a420 log in the river
better logs for this API
2025-10-10 15:21:36 -07:00
SpudGunMan
bc06712b87 enhance rssread
enhance rssread
enhance rssread
enhance rssread
2025-10-10 12:50:07 -07:00
SpudGunMan
260e52fe81 Update system.py 2025-10-10 11:53:33 -07:00
SpudGunMan
6b548f82b2 Update mesh_bot.py 2025-10-10 11:40:30 -07:00
SpudGunMan
2273b481ad NEW CHUNKER
what day is it, chunker day!
2025-10-10 11:38:01 -07:00
SpudGunMan
95ee7779b4 Update mesh_bot.py
@mesb1 ahhh thanks!
2025-10-10 09:47:32 -07:00
SpudGunMan
ee1391f6e7 2fa.ini 2025-10-10 07:41:18 -07:00
SpudGunMan
b7a0d7cd8e Update system.py 2025-10-10 07:30:18 -07:00
SpudGunMan
a880236117 Update filemon.py 2025-10-10 07:24:42 -07:00
SpudGunMan
a67bdc3641 Update filemon.py 2025-10-10 07:16:02 -07:00
SpudGunMan
da8235adae Update filemon.py 2025-10-10 07:08:51 -07:00
SpudGunMan
22384463e2 are you human
or are you dancer, this was just fun to add. 2fa human check to x: commands
2025-10-10 07:03:10 -07:00
SpudGunMan
b48377de5f Update system.py 2025-10-10 00:42:31 -07:00
SpudGunMan
855c2e08cc Update system.py 2025-10-10 00:41:30 -07:00
SpudGunMan
d0aa07ed7d Update system.py
i should snooze
2025-10-10 00:39:43 -07:00
SpudGunMan
7328a92535 Update system.py 2025-10-10 00:36:34 -07:00
SpudGunMan
35c8dc6f70 resetLeaderboard 2025-10-10 00:36:22 -07:00
SpudGunMan
4b1123dcac Update mesh_bot.py 2025-10-10 00:06:19 -07:00
SpudGunMan
b74dc1ff25 not this or that 2025-10-09 23:31:21 -07:00
SpudGunMan
8c752dff3e Update system.py 2025-10-09 23:11:08 -07:00
SpudGunMan
b3b45a4335 Update system.py 2025-10-09 19:10:58 -07:00
SpudGunMan
fd86187798 Update mesh_bot.py 2025-10-09 19:09:20 -07:00
SpudGunMan
4da3e68c62 readrss
thanks FJRP you can now return an rss feed
2025-10-09 19:06:38 -07:00
SpudGunMan
e47907ebeb Update system.py 2025-10-09 18:38:35 -07:00
SpudGunMan
f63278ae8f Update system.py 2025-10-09 18:37:04 -07:00
SpudGunMan
0e0d2f11d7 Update system.py 2025-10-09 18:11:40 -07:00
SpudGunMan
6587ba61e2 Update system.py 2025-10-09 18:10:02 -07:00
SpudGunMan
b46697c0c4 enhance leaderboard 2025-10-09 18:08:38 -07:00
SpudGunMan
f5f8539924 segassem
reverse the order of the messages
2025-10-09 17:21:07 -07:00
SpudGunMan
e8063fcf3f Update system.py 2025-10-09 17:05:01 -07:00
SpudGunMan
169f9b27a5 Update system.py 2025-10-09 17:03:39 -07:00
SpudGunMan
4ceb23bcff Update system.py 2025-10-09 17:02:14 -07:00
SpudGunMan
315ae84bb6 enhance 2025-10-09 16:27:06 -07:00
SpudGunMan
fb12c11a7e Update mesh_bot.py 2025-10-09 15:39:32 -07:00
SpudGunMan
496c222cdc Update README.md 2025-10-09 15:34:56 -07:00
Kelly
cb55aba498 Merge pull request #203 from SpudGunMan/copilot/add-local-wiki-search-functionality
Add Kiwix local wiki server support for offline Wikipedia searches
2025-10-08 20:38:17 -07:00
SpudGunMan
1aac3d5ac2 failover 2025-10-08 20:37:21 -07:00
SpudGunMan
413f2a24d9 Kiwix in wiki
@NomDeTom its finally done
2025-10-08 20:29:52 -07:00
SpudGunMan
c75782d559 Update system.py 2025-10-08 20:03:05 -07:00
SpudGunMan
d38314a21c Update system.py 2025-10-08 19:31:30 -07:00
SpudGunMan
65dfe90edc Update system.py 2025-10-08 19:24:14 -07:00
SpudGunMan
3ce24fb7c9 Update system.py 2025-10-08 12:48:26 -07:00
SpudGunMan
8765e5a871 🥒
ahh pickles
2025-10-08 12:45:37 -07:00
SpudGunMan
b4f0421423 Update system.py 2025-10-08 12:37:52 -07:00
SpudGunMan
2b8906ae55 colon
KFC🍗
2025-10-08 11:11:14 -07:00
Kelly
5710b47a99 Merge pull request #208 from SpudGunMan/copilot/add-funny-data-facts-logging
Add mesh leaderboard feature to track extreme metrics and special packets
2025-10-08 11:02:42 -07:00
SpudGunMan
1f8bf5a700 bytes&bits 2025-10-08 11:01:21 -07:00
copilot-swe-agent[bot]
171480b704 Add leaderboard command documentation to README
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 16:53:07 +00:00
copilot-swe-agent[bot]
c74e4f99b2 Add mesh leaderboard feature to track extreme metrics
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 16:50:46 +00:00
copilot-swe-agent[bot]
cd5749521c Initial plan 2025-10-08 16:43:39 +00:00
Kelly
5ce7019dba Merge pull request #204 from SpudGunMan/copilot/fix-messages-bot-errors
Fix messages command error with Unicode characters (Cyrillic) I like this approach thanks GitHub
2025-10-08 09:21:22 -07:00
SpudGunMan
6c1e0cc2f9 bits&bytes 2025-10-08 09:20:09 -07:00
SpudGunMan
68b171f68e Update mesh_bot.py 2025-10-08 08:42:44 -07:00
SpudGunMan
7cfd5d0b0e Update mesh_bot.py 2025-10-08 08:39:24 -07:00
SpudGunMan
6dd4f0c4b6 Update joke.py 2025-10-08 08:37:03 -07:00
copilot-swe-agent[bot]
8ef0fa2ac0 Fix messages command to handle Unicode characters safely
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:18:30 +00:00
copilot-swe-agent[bot]
0c8d6b8fac Initial plan 2025-10-08 15:12:01 +00:00
copilot-swe-agent[bot]
1e4e5e6627 Add documentation and fix deprecated BeautifulSoup method
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:05:09 +00:00
copilot-swe-agent[bot]
c97004b410 Add Kiwix local wiki server support with configuration options
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:00:44 +00:00
copilot-swe-agent[bot]
2292fb2655 Initial plan 2025-10-08 14:54:32 +00:00
SpudGunMan
3ebf3ba374 Update config.template 2025-10-08 07:05:40 -07:00
SpudGunMan
b6087c926c news.template 2025-10-08 01:22:08 -07:00
SpudGunMan
2895e6c034 newNews🚨🚨
the location of news.txt changed FYI 🚨 now you can read more files
2025-10-08 01:14:10 -07:00
SpudGunMan
691bc8d701 Update README.md 2025-10-08 00:02:08 -07:00
SpudGunMan
bd50524e95 Update README.md 2025-10-07 23:53:22 -07:00
SpudGunMan
299b749f0e Update survey.py
I should sleep
2025-10-07 23:44:52 -07:00
SpudGunMan
9a060e3c6e Update quiz.py 2025-10-07 23:43:34 -07:00
SpudGunMan
a012ef17d0 Update survey.py 2025-10-07 23:41:22 -07:00
SpudGunMan
adbf78b740 enhance 2025-10-07 23:39:23 -07:00
SpudGunMan
3aad8d89cf Update survey.py 2025-10-07 23:34:36 -07:00
SpudGunMan
3370304249 Update survey.py 2025-10-07 23:30:35 -07:00
SpudGunMan
ef62a06db1 Update settings.py
disabled till configured and also uses io
2025-10-07 23:16:28 -07:00
SpudGunMan
8cc1d24b93 Update README.md 2025-10-07 23:15:23 -07:00
SpudGunMan
fca90cbee3 Update survey.py 2025-10-07 23:10:09 -07:00
SpudGunMan
d05c7bb6a5 Update survey.py 2025-10-07 23:05:12 -07:00
SpudGunMan
7774529fb4 bugfix 2025-10-07 23:02:52 -07:00
SpudGunMan
4c615af22d Update mesh_bot.py 2025-10-07 22:42:13 -07:00
SpudGunMan
6c078b4d17 Survey Says!
is this cool?
2025-10-07 22:39:08 -07:00
SpudGunMan
ddb9c8b4bf Update mesh_bot.py 2025-10-07 20:34:17 -07:00
SpudGunMan
73f3175705 Update mesh_bot.py 2025-10-07 20:18:12 -07:00
SpudGunMan
d2ee1bce1c Update quiz.py 2025-10-07 20:01:20 -07:00
SpudGunMan
b4a2149815 enhance 2025-10-07 20:00:22 -07:00
SpudGunMan
320f41e05a documentation 2025-10-07 17:58:19 -07:00
SpudGunMan
48a57e875f QuizMaster
let me know if this is cool
2025-10-07 17:48:22 -07:00
SpudGunMan
ce317d8bbe Update simulator.py 2025-10-07 16:35:03 -07:00
SpudGunMan
c2d2a8f7e4 Update simulator.py 2025-10-07 16:33:33 -07:00
SpudGunMan
00280e351c Update mesh_bot.py 2025-10-07 14:04:07 -07:00
SpudGunMan
0e8bb197a9 Update mesh_bot.py 2025-10-07 13:59:49 -07:00
SpudGunMan
d825c0fa15 Update mesh_bot.py
what happened here? I forget now but sheesh!
2025-10-07 13:57:00 -07:00
SpudGunMan
6abe73c1bc Update mesh_bot.py
ack
2025-10-07 13:54:32 -07:00
SpudGunMan
b8e9adb223 fixMessagesCommand
thanks @mesb1  https://github.com/SpudGunMan/meshing-around/issues/200
2025-10-07 13:48:23 -07:00
SpudGunMan
e621016e9a nom
nom
2025-10-07 06:06:21 -07:00
SpudGunMan
cfaf652852 Update mesh_bot.py 2025-10-06 20:02:36 -07:00
SpudGunMan
6c27b5d5de xoxo
enhance
2025-10-06 18:03:22 -07:00
SpudGunMan
a31fa90942 Update system.py 2025-10-06 14:57:40 -07:00
SpudGunMan
3cd347dff3 Update tictactoe.py 2025-10-06 14:46:24 -07:00
SpudGunMan
ea4ac1f9c1 whichonelooksbetter 2025-10-06 14:42:50 -07:00
SpudGunMan
a9da8336cc enhance 2025-10-06 14:40:08 -07:00
SpudGunMan
4ba60ed276 correctLogLevel 2025-10-06 14:25:13 -07:00
Kelly
42e07d44e6 Merge pull request #198 from martinbogo/feature/tictactoe-game
Feature/tictactoe game
2025-10-06 14:11:23 -07:00
SpudGunMan
11f5218c2e Merge branch 'feature/tictactoe-game' of https://github.com/martinbogo/meshing-around into pr/198 2025-10-06 14:08:49 -07:00
SpudGunMan
e137420138 patch-2 2025-10-06 14:08:07 -07:00
Kelly
80c0f698b6 Update modules/games/tictactoe.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 13:51:56 -07:00
SpudGunMan
2045bf98f7 🧩 2025-10-06 13:45:02 -07:00
SpudGunMan
c36ce2c3a6 Update mesh_bot.py 2025-10-06 13:09:47 -07:00
SpudGunMan
7ff36a3d5f Update mesh_bot.py 2025-10-06 13:05:32 -07:00
SpudGunMan
ae1a3040b5 patches
dont need no stinking patches. thanks again.
2025-10-06 12:54:41 -07:00
Martin Bogomolni
84b6b48d60 feat: Add tic-tac-toe game with compact messaging
🎮 New Tic-Tac-Toe Game Features:
- Compact 3x3 board display using ASCII art
- Smart AI opponent with win/block/random strategy
- All messages under 200-character meshtastic limit (tested: 10-50 chars)
- Player vs Bot gameplay with X (player) vs O (bot)
- Win detection for rows, columns, and diagonals
- Tie game detection when board is full
- Game statistics tracking (games played, won)

🔧 Integration Features:
- Follows established game patterns from hangman/hamtest
- Added to restrictedCommands (DM-only like other games)
- Integrated with game tracker system for memory cleanup
- Added configuration option in config.template
- Automatic cleanup of stale game sessions

🎯 Game Mechanics:
- Players pick positions 1-9 corresponding to board layout
- Simple input parsing (extracts first digit from message)
- Graceful error handling for invalid moves
- 'end' command to quit game
- Automatic game cleanup on completion

📊 Message Examples:
- New game: 39 chars
- Game moves: 50 chars
- Win/lose: 40 chars
- Invalid move: 23 chars
- All well under 200-char limit

Tested: Complete game scenarios, AI behavior, message lengths
Follows: Existing game implementation patterns and memory management
2025-10-06 00:08:56 -07:00
Martin Bogomolni
9f3446b605 feat: Implement comprehensive memory management and stability improvements
🔧 Memory Management Enhancements:
- Add memory cleanup constants (MAX_CMD_HISTORY=1000, MAX_SEEN_NODES=500, MAX_MSG_HISTORY=100)
- Implement cleanup_memory() function to prevent unbounded list growth
- Add periodic cleanup every hour via watchdog process
- Clean up stale game tracker entries automatically
- Limit cmdHistory and msg_history sizes to prevent memory bloat

🚀 Async Task Management Improvements:
- Fix async task management in both mesh_bot.py and pong_bot.py
- Implement proper task cleanup and cancellation on shutdown
- Add task names for better debugging and monitoring
- Use asyncio.gather() with return_exceptions=True for better error handling
- Prevent task hanging and resource leaks

🛡️ Enhanced Resource Management:
- Improve exit_handler() with proper interface cleanup
- Add atexit.register() for automatic graceful shutdown
- Ensure all meshtastic interfaces are properly closed
- Save persistent data (BBS, email, SMS, game scores) on exit
- Perform final memory cleanup during shutdown

🔍 Better Exception Handling:
- Replace bare except: blocks with specific exception handling
- Add proper error logging throughout the codebase
- Improve BBS database operations with better error recovery
- Add try/catch blocks for file operations and imports

📈 System Stability Improvements:
- Prevent memory leaks from growing lists and dictionaries
- Add automatic cleanup of stale player tracking data
- Improve error recovery in watchdog and async loops
- Better handling of interface connection failures

These changes address critical memory management issues that could cause
the bot to consume increasing memory over time, eventually leading to
system instability. The improvements ensure long-term reliability and
better resource utilization.

Fixes: Memory leaks, async task hanging, resource cleanup issues
Improves: System stability, error handling, resource management
Tested: Code analysis and review completed
2025-10-05 23:45:40 -07:00
SpudGunMan
10add3147d Update system.py 2025-10-05 23:10:23 -07:00
SpudGunMan
4e074a309f Update system.py 2025-10-05 23:05:55 -07:00
SpudGunMan
f394f58b9f paxCounter 2025-10-05 22:30:03 -07:00
SpudGunMan
30bcee498d Update config.template 2025-10-05 22:24:35 -07:00
SpudGunMan
5c54ce0b70 better waypoint data 2025-10-05 21:50:56 -07:00
SpudGunMan
b4684f49ff Update system.py 2025-10-05 21:38:19 -07:00
SpudGunMan
c6c1e9f637 refactor more consumeMetadata 2025-10-05 21:13:39 -07:00
SpudGunMan
54540b1656 cleanup 2025-10-05 20:26:48 -07:00
SpudGunMan
006c9f58c6 enhance bbsLink 2025-10-05 19:44:00 -07:00
SpudGunMan
3d582e9b77 enhance import of BBS
for ssh copy
2025-10-05 19:20:44 -07:00
SpudGunMan
0578c0b233 more enhancing metadata 2025-10-05 19:00:34 -07:00
SpudGunMan
98ccf8708f enhanceTelemetry
logging and handlers for telemetry
2025-10-05 17:47:19 -07:00
SpudGunMan
47280f4330 Update bbstools.py
comment from https://github.com/SpudGunMan/meshing-around/issues/194
2025-10-05 15:26:50 -07:00
SpudGunMan
4b0b074ba7 mathWasntMathn'
@mesb1 thanks
2025-10-05 09:08:47 -07:00
SpudGunMan
d8d79f46b5 Update README.md
@pdxlocations
2025-10-04 15:45:43 -07:00
SpudGunMan
1a49a81cf5 load_bbsdb with 'api'
allows for ssh file synch?
2025-10-04 12:54:20 -07:00
SpudGunMan
5c0b04f0b7 Update injectDM.py 2025-10-03 19:11:40 -07:00
SpudGunMan
8f1cb6265d Update injectDM.py 2025-10-03 19:07:25 -07:00
SpudGunMan
c51a4584ae enhance bbsAPI
switch for diskIO
2025-10-03 19:06:14 -07:00
SpudGunMan
13ee6d4fd6 enhance bbsDM
enable a new 'API' to inject into the pkl file for DM's also see 2d44faac98
2025-10-03 19:02:15 -07:00
SpudGunMan
2d44faac98 Create injectDM.py
Usage: python3 script/injectDM.py -s NODEID -d NODEID -m "message"
2025-10-03 18:15:57 -07:00
SpudGunMan
0a2daeac1f Update joke.py 2025-10-03 15:42:05 -07:00
SpudGunMan
da7ba256d8 Update joke.py 2025-10-03 15:41:40 -07:00
SpudGunMan
42e99a0dc1 Update joke.py 2025-10-03 15:40:54 -07:00
SpudGunMan
ca5896a015 Update locationdata.py 2025-10-03 15:35:04 -07:00
SpudGunMan
ebe2636104 refactor riverflow 2025-10-03 15:34:03 -07:00
SpudGunMan
971a421d01 riverflow refactor 2025-10-03 15:15:40 -07:00
SpudGunMan
9d080de8f3 Update locationdata.py 2025-10-03 15:01:41 -07:00
SpudGunMan
5209092928 better riverflow logic 2025-10-03 14:54:12 -07:00
SpudGunMan
6439f49fb1 Update filemon.py 2025-10-03 12:28:47 -07:00
SpudGunMan
c115cdf82f enhance x: with subprocess 2025-10-03 12:15:14 -07:00
SpudGunMan
63fccbdf3e Update config.template 2025-10-03 12:01:41 -07:00
SpudGunMan
c23564d8b5 Update system.py 2025-10-02 19:37:57 -07:00
SpudGunMan
0b9db28951 Update README.md 2025-10-02 19:18:39 -07:00
SpudGunMan
1aa4eddb3b Update filemon.py 2025-10-02 19:07:37 -07:00
SpudGunMan
a7de64b385 Update filemon.py 2025-10-02 19:03:15 -07:00
SpudGunMan
2e8206d4ec x:ShellCommands
this x: is a direct shell access from DM, to enable it needs the enable_runShellCmd, allowXcmd, xcmdChannel set. Make sure your secure.
2025-10-02 18:58:27 -07:00
SpudGunMan
b47c13503b Update filemon.py 2025-10-02 13:37:17 -07:00
SpudGunMan
a66dbd13fd Update config.template 2025-10-02 13:17:48 -07:00
SpudGunMan
02322cdf91 cleanup
fixes per https://github.com/SpudGunMan/meshing-around/issues/192
2025-10-02 04:51:40 -07:00
SpudGunMan
955d3681e9 Update joke.py 2025-10-01 11:41:59 -07:00
SpudGunMan
9d96c02870 catch em all 2025-09-29 16:59:13 -07:00
SpudGunMan
c87dba1e06 fixWikiErrors 2025-09-29 16:58:34 -07:00
SpudGunMan
1c3d2f7f18 Update mesh_bot.py 2025-09-29 16:16:33 -07:00
SpudGunMan
b53a7d3832 Update joke.py 2025-09-29 16:13:01 -07:00
SpudGunMan
99e74ae8c0 lower! 2025-09-28 18:22:56 -07:00
SpudGunMan
1bdfc3828f cleanup 2025-09-28 18:20:42 -07:00
SpudGunMan
26f39e76e6 somdays tabs killl me 2025-09-28 17:41:13 -07:00
SpudGunMan
c49dcfbfc8 aarg 2025-09-28 17:38:59 -07:00
SpudGunMan
1008ec6afa yarp 2025-09-28 17:36:57 -07:00
SpudGunMan
8c0a1bbd0d cleanup
need to fix this
2025-09-28 17:27:55 -07:00
SpudGunMan
30d8f00aeb Update update.sh 2025-09-28 17:25:31 -07:00
SpudGunMan
033b1bcd51 Update update.sh 2025-09-28 17:21:00 -07:00
SpudGunMan
d80b2da06a Update launch.sh 2025-09-28 17:18:56 -07:00
SpudGunMan
dc02464662 Update update.sh 2025-09-28 17:17:53 -07:00
SpudGunMan
b738881ff1 Update update.sh 2025-09-28 17:16:04 -07:00
SpudGunMan
b8f0601684 enhance 2025-09-28 17:11:09 -07:00
SpudGunMan
910c045b08 Update update.sh 2025-09-28 17:07:12 -07:00
SpudGunMan
35ba139577 enhance 2025-09-28 17:03:59 -07:00
SpudGunMan
eddd990cc5 Update README.md
ffs
2025-09-28 16:32:40 -07:00
SpudGunMan
6e3d83401f enhance echo
enhance echo
2025-09-28 16:28:31 -07:00
SpudGunMan
f9ab6a79d3 echo
echo command will just echo, off by default its handy for things like making a demo or node speak
2025-09-28 15:04:56 -07:00
SpudGunMan
4b2402c286 Update space.py 2025-09-27 17:24:21 -07:00
SpudGunMan
47e0276f0c howtall
returns height of something you give a shadow by using sun angle
2025-09-26 19:23:12 -07:00
SpudGunMan
6dc54abf43 enhance 2025-09-26 19:16:16 -07:00
SpudGunMan
53ff37c782 howtall
is this handy? I thought it might be for tree tower use
2025-09-26 19:02:02 -07:00
SpudGunMan
5c48e008ee Update checklist.py 2025-09-26 19:00:46 -07:00
SpudGunMan
ddac18bb13 Update README.md 2025-09-26 16:13:34 -07:00
SpudGunMan
507919bb4c aarg 2025-09-26 16:05:55 -07:00
SpudGunMan
b4f3f9887d OCD 2025-09-26 15:58:10 -07:00
SpudGunMan
aa051abbd4 better Logic for handler 2025-09-26 15:54:51 -07:00
SpudGunMan
8ba0c6f14c Update system.py 2025-09-26 15:47:51 -07:00
SpudGunMan
166b15463a Update launch.sh 2025-09-26 15:46:36 -07:00
SpudGunMan
d02924bfda Update launch.sh 2025-09-26 15:45:26 -07:00
SpudGunMan
0a123251f4 Update README.md 2025-09-26 15:44:23 -07:00
SpudGunMan
a80f926d08 Update launch.sh 2025-09-26 15:43:29 -07:00
SpudGunMan
b8318f8f3e Update README.md 2025-09-26 15:38:36 -07:00
SpudGunMan
adb6fa3b5a Update system.py 2025-09-26 15:36:49 -07:00
SpudGunMan
a9254c9c79 addFav
Helper Script to Add Favorite to the bot node for admin and other use
2025-09-26 15:32:24 -07:00
SpudGunMan
0ac642ac44 Update config.template 2025-09-24 15:21:02 -07:00
SpudGunMan
ec24a8b8dd typo 2025-09-24 15:19:57 -07:00
SpudGunMan
f79026a95f enhance
DEBUGpackets, debugMetadata hidden config.ini values
2025-09-24 15:16:37 -07:00
SpudGunMan
99acaf28a1 Update system.py 2025-09-24 15:14:17 -07:00
SpudGunMan
9c068c8d28 Update settings.py 2025-09-24 15:14:07 -07:00
SpudGunMan
d9ab1b88c1 Update system.py 2025-09-24 15:10:26 -07:00
SpudGunMan
8499b6c851 Update README.md 2025-09-19 09:36:28 -07:00
SpudGunMan
ea47bf9329 bugfix parser
thanks again Iris!
2025-09-19 09:32:53 -07:00
SpudGunMan
229043c32a fix config.ini bug
Thanks Iris for pointing out this long time bug
2025-09-19 09:05:37 -07:00
SpudGunMan
5dbd137f14 enhance 2025-09-19 08:51:43 -07:00
SpudGunMan
ca83117180 Update locationdata.py
fix polygone at 4+
2025-09-17 12:13:03 -07:00
SpudGunMan
fde37313f5 Update locationdata.py 2025-09-17 11:55:34 -07:00
Kelly
a39f588f9f Merge pull request #188 from SpudGunMan/lab
Noisy Telemetry
lost work on location data
2025-09-17 11:52:50 -07:00
SpudGunMan
39e348f701 Update locationdata.py
got lost somehow
2025-09-17 11:43:43 -07:00
SpudGunMan
4fdfa49b87 Revert "Update locationdata.py"
This reverts commit 8a64b8e7ad.
2025-09-17 11:43:22 -07:00
SpudGunMan
8a64b8e7ad Update locationdata.py
this got lost somewhere
2025-09-17 11:41:35 -07:00
SpudGunMan
c585f60882 Update README.md
think this was a typo
2025-09-17 10:55:38 -07:00
SpudGunMan
4e91801cb9 Update system.py
reset alerts
2025-09-17 10:55:30 -07:00
SpudGunMan
4e1d3e2b58 lower volume 2025-09-17 10:55:17 -07:00
SpudGunMan
38d5006236 Update system.py 2025-09-16 17:24:55 -07:00
SpudGunMan
642738e3b6 noisyNodeLogging
telemetry logger idea
2025-09-16 17:09:47 -07:00
SpudGunMan
5728d6b9e3 Update locationdata.py
didnt like this
2025-09-13 18:25:35 -07:00
SpudGunMan
82bec43f22 Update locationdata.py
move this and add miles
2025-09-13 18:21:09 -07:00
SpudGunMan
7ca8b6793a Update locationdata.py
limit noaa which just gave me a 11 digit richter ooof
2025-09-13 17:33:18 -07:00
SpudGunMan
d589d3e155 Update system.py 2025-09-11 18:00:42 -07:00
SpudGunMan
452c4aa520 Update locationdata.py 2025-09-11 17:50:19 -07:00
SpudGunMan
e5d2ea4bcb Update system.py 2025-09-10 19:24:44 -07:00
SpudGunMan
2596d133fd Update system.py 2025-09-10 19:23:08 -07:00
SpudGunMan
c3221d64a8 enhance 2025-09-10 19:03:44 -07:00
SpudGunMan
dcabfc0f50 Update system.py 2025-09-10 19:00:37 -07:00
SpudGunMan
2eca5f644a add aircraft lookup to highFlying info
@Cisien for the idea
2025-09-10 18:58:33 -07:00
SpudGunMan
3f90a7fc39 dedupe emergency alerts
double check we didnt already send the message here it could be duplicated elsewhere
2025-09-10 18:06:43 -07:00
SpudGunMan
9639c793d9 Update README.md
i can spell well
2025-09-10 13:09:54 -07:00
SpudGunMan
18294f4ca3 Update locationdata.py 2025-09-10 12:48:33 -07:00
SpudGunMan
14043dd950 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-09-10 12:46:25 -07:00
SpudGunMan
fe1c264b19 Update locationdata.py 2025-09-10 12:46:01 -07:00
Kelly
8848d9b6fe Merge pull request #184 from ulab/patch-2
Add fileMon files to .gitignore
2025-09-10 09:49:28 -07:00
Kelly
daa6e85318 Merge pull request #185 from sodoku/dealert-fixes
fix: wrong variable use
2025-09-10 09:49:01 -07:00
sodoku
f7f127590d fix: wrong variable use 2025-09-10 18:41:45 +02:00
Balu
5b0fd65d31 Add fileMon files to .gitignore 2025-09-10 14:52:19 +02:00
SpudGunMan
fe12e1f107 enhance 2025-09-09 15:55:34 -07:00
SpudGunMan
11025f101f add QRN to hfcond 2025-09-09 15:19:46 -07:00
SpudGunMan
92a5fc2ed5 Update space.py 2025-09-09 15:03:37 -07:00
SpudGunMan
3f891d93d2 add space
same here to stop runaway commands
2025-09-09 15:03:22 -07:00
SpudGunMan
398c9ddb60 remove space
this happens to stop a multi-bot runaway
2025-09-09 15:02:37 -07:00
SpudGunMan
518eb25f0d Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-09-09 15:00:54 -07:00
SpudGunMan
0613cc7b3d Update README.md 2025-09-09 15:00:51 -07:00
Kelly
be2d1fbf72 Merge pull request #179 from ulab/space_spaces
Add some spaces around emojiis and after punctuation
2025-09-09 13:26:46 -07:00
Kelly
8595190ed1 Merge pull request #181 from SpudGunMan/lab
HowFar
2025-09-09 13:26:04 -07:00
SpudGunMan
5703cfb381 🗺️ howfar 2025-09-09 12:42:03 -07:00
SpudGunMan
ff9b76c966 Update locationdata.py 2025-09-09 12:08:59 -07:00
Balu
e41f692038 Add some spaces around emojiis and after punctuation 2025-09-09 14:06:27 +02:00
SpudGunMan
19935d9f08 howfar
initial idea goofin to see if this works
2025-09-07 19:12:57 -07:00
SpudGunMan
f0e8b2c057 🐛 2025-09-02 11:19:23 -07:00
SpudGunMan
eb2d809fe4 from: in bbs_read_message
Reading Messages has the last 4 of the HexID now "from"
2025-08-31 20:37:40 -07:00
SpudGunMan
b276bbb40a rlist command in help 2025-08-31 19:47:59 -07:00
SpudGunMan
35ea7cb505 fix HTML parsing for rlist getRepeaterBook
Thanks to rhinodods on discord for the alert
2025-08-31 18:02:21 -07:00
SpudGunMan
eb78c2e5e8 Update install.sh 2025-08-29 17:48:30 -07:00
SpudGunMan
3df16b7626 Update system.py 2025-08-29 17:25:51 -07:00
SpudGunMan
8c01433d14 🚨fixes/enhancments
better multi network control of alerts and 🪫 alert to mesh
2025-08-27 12:10:38 -07:00
SpudGunMan
a8dbef7e12 Update install.sh 2025-08-26 17:24:29 -07:00
SpudGunMan
23478812e0 Update install.sh 2025-08-26 16:51:33 -07:00
SpudGunMan
08c2c668f9 EarthQuake
from USGS data seismicportal.eu wont let me set a radius on lookups but looking into it
2025-08-26 14:21:10 -07:00
SpudGunMan
3b41e39ff5 Update system.py 2025-08-26 14:09:23 -07:00
SpudGunMan
78fa3209e6 Update install.sh
update gemma3:270m
2025-08-26 11:04:46 -07:00
Kelly
c5ba56a656 Merge pull request #178 from SpudGunMan/lab
Gemma3 LLM
2025-08-26 11:00:31 -07:00
SpudGunMan
50c3249edc explicitCmd
i think @NomDeTom mentioned this a long time ago and well .. here is a change to help the phases of the moon and tide.
2025-08-26 10:44:30 -07:00
SpudGunMan
80897f7a82 defaults to gemma3 raw input
this is a change as its looking to remove google lookups with the python module. if this changes is impacting please let me know in [general] add `rawLLMQuery = False` to reverse
2025-08-26 10:26:33 -07:00
SpudGunMan
d311832a92 truncation 2025-08-20 11:55:44 -07:00
SpudGunMan
56af59345d Update settings.py 2025-08-19 06:19:07 -07:00
SpudGunMan
c1adca7db0 Update mesh_bot.py 2025-08-18 18:55:23 -07:00
SpudGunMan
4c7fe55b43 hopFix 2025-08-18 18:54:02 -07:00
SpudGunMan
df6a1cfb66 Update pong_bot.py
better MQTT handler
2025-08-17 20:25:40 -07:00
SpudGunMan
9994446510 better MQTT handler 2025-08-17 20:24:09 -07:00
SpudGunMan
9272218815 Update system.py
i declare
2025-08-17 20:05:41 -07:00
SpudGunMan
388d862fc9 Update llm.py 2025-08-16 21:24:46 -07:00
SpudGunMan
ac33f8a02b Update llm.py 2025-08-16 21:16:44 -07:00
SpudGunMan
f04392a81c Update llm.py 2025-08-16 20:25:53 -07:00
SpudGunMan
d0097c092b Update llm.py 2025-08-16 17:48:04 -07:00
SpudGunMan
92ff166260 Update llm.py
adding these back, the token limit just has no bounds
2025-08-15 21:48:30 -07:00
Kelly
bfe0d219f9 Merge pull request #175 from SpudGunMan/main
fix daylight
2025-08-15 21:44:24 -07:00
SpudGunMan
85a2d90cff fix daylight 2025-08-15 21:42:45 -07:00
SpudGunMan
e15232875c token limit 2025-08-15 21:33:40 -07:00
SpudGunMan
d1a87f161b updateOllama
remember to update to latest ollama bin's
2025-08-15 21:17:58 -07:00
SpudGunMan
626ac59b4e gemma3
LLM rewrite, the removal of RAG to keep things clean.

This default changes as well as puts input direct to the LLM further testing is needed, new LLM prompting is different.
2025-08-15 21:11:09 -07:00
SpudGunMan
835a9e5f89 Update space.py 2025-08-15 06:52:55 -07:00
SpudGunMan
3ae928dd66 more light on the sun 2025-08-15 06:51:51 -07:00
SpudGunMan
3973406783 formatting of sun
trying this out vs the old way
2025-08-15 06:32:15 -07:00
SpudGunMan
4fbdd42837 Update space.py 2025-08-15 05:40:33 -07:00
SpudGunMan
04378efdd8 Update space.py 2025-08-14 20:33:21 -07:00
SpudGunMan
0d19a40ed6 Update space.py 2025-08-14 19:47:53 -07:00
SpudGunMan
75ac3c974a Update space.py 2025-08-14 19:47:25 -07:00
SpudGunMan
7e0eb348ae 🌝 2025-08-14 19:46:53 -07:00
SpudGunMan
af6ea2a512 Update space.py 2025-08-14 19:41:12 -07:00
SpudGunMan
6665ea7dcd moon refactor 2025-08-14 19:40:32 -07:00
SpudGunMan
3212661ee8 enhance sun and moon
add position data when visible
2025-08-14 19:35:13 -07:00
SpudGunMan
0675132171 up a river
without help
2025-08-13 20:41:42 -07:00
Kelly
fdb7897963 Merge pull request #173 from dludwig/typofix
typo fix deteted -> detected
2025-08-13 20:16:18 -07:00
dludwig
8ff7a0bf3c typo fix detetec -> detected 2025-08-13 15:18:34 -07:00
SpudGunMan
c210534543 Update README.md 2025-08-13 08:58:31 -07:00
SpudGunMan
ea7574a868 Update locationdata.py
remove the $$ end marker
2025-08-12 13:40:26 -07:00
SpudGunMan
8f69c4d93c Update mesh_bot.py
aarg
2025-08-12 12:03:43 -07:00
SpudGunMan
bc9ada91b4 Update mesh_bot.py 2025-08-12 11:54:17 -07:00
SpudGunMan
28f06f0a21 Update config.template 2025-08-12 11:50:05 -07:00
SpudGunMan
267fe392e3 tuypo 2025-08-12 11:42:13 -07:00
SpudGunMan
6c1f7940ca refactor coastal weather
changes to config.ini template if you use tide or mwx
2025-08-12 11:35:42 -07:00
SpudGunMan
2fc9281394 Update system.py 2025-08-04 18:54:46 -07:00
SpudGunMan
b5bd1008c2 HowHigh? divideBy3
https://github.com/SpudGunMan/meshing-around/discussions/170
2025-08-03 17:43:10 -07:00
SpudGunMan
ee1db5b7be Update locationdata.py 2025-08-02 19:21:46 -07:00
SpudGunMan
7395b96337 Update locationdata.py 2025-07-30 10:16:33 -07:00
SpudGunMan
f3c6f77b23 Update mesh_bot.py 2025-07-30 10:13:14 -07:00
SpudGunMan
f6e04a42a0 Update system.py 2025-07-30 10:11:34 -07:00
SpudGunMan
3fcd588d02 bugs and docs
Consolidated Tide with MWX fixed up readme and cleaned up rlist in help
2025-07-30 10:05:04 -07:00
SpudGunMan
e1b47484f2 NOAA Coastal Marine Forcast data
using older but handy products with new mwx
2025-07-30 08:34:20 -07:00
SpudGunMan
14798cb992 alertDe 2025-07-22 09:28:08 -07:00
Kelly
41c8f0044b Merge pull request #163 from SudoRand/efficient-chunking
Allow chunker to consolidate lines when possible
2025-07-22 08:23:42 -07:00
SpudGunMan
45eefb24d8 enhance retry 2025-07-22 07:02:08 -07:00
SpudGunMan
410d32947c Update system.py 2025-07-21 20:13:07 -07:00
SpudGunMan
748652ac62 onDisconnect
correcting multiple issues adding config.ini feature for dont_retry_disconnect

https://github.com/SpudGunMan/meshing-around/issues/137
https://github.com/SpudGunMan/meshing-around/issues/156
2025-07-21 20:10:59 -07:00
SpudGunMan
d715cb6b4d Update system.py 2025-07-21 04:33:04 -07:00
SpudGunMan
1895a365ae fix retry and failure
correcting multiple issues with some bad code

https://github.com/SpudGunMan/meshing-around/issues/137

https://github.com/SpudGunMan/meshing-around/issues/156
2025-07-20 05:41:50 -07:00
SpudGunMan
cc58a38165 Update system.py 2025-07-18 21:24:55 -07:00
SpudGunMan
a8ccb05d56 Update system.py
bug of undefined for interface retry
2025-07-16 08:57:39 -07:00
SpudGunMan
a90a533a30 USGS Alerts
documented
2025-07-15 21:20:37 -07:00
SpudGunMan
57a4e5d68c Update README.md 2025-07-15 21:12:23 -07:00
SpudGunMan
7c99b684ad riverflow
never got documented well
2025-07-15 21:11:48 -07:00
SpudGunMan
b957c89d70 Update README.md 2025-07-15 20:32:51 -07:00
SpudGunMan
9b986dd57a Update locationdata.py
allow FIPS only
2025-07-15 20:30:43 -07:00
SpudGunMan
9e348332e5 SAME code back in iPAWS
the state only FIPS codes are too wide
2025-07-15 19:09:27 -07:00
SpudGunMan
0cfe759ef6 Update mesh_bot.py 2025-07-15 15:34:32 -07:00
SpudGunMan
e95902ef98 fix Excessive queries to FEMA
issue raised https://github.com/SpudGunMan/meshing-around/issues/165

Co-Authored-By: DEVAFRS <180097515+devafrs@users.noreply.github.com>
2025-07-15 15:22:53 -07:00
SpudGunMan
c7df4d88d1 Update config.template
Co-Authored-By: Russell Schmidt <836646+rfschmid@users.noreply.github.com>
2025-07-15 14:28:25 -07:00
SpudGunMan
6d01c5a986 further adjustments for 2.7.2
https://github.com/SpudGunMan/meshing-around/pull/164#pullrequestreview-3021705851

and

https: //github.com/SpudGunMan/meshing-around/issues/162
Co-Authored-By: SudoRand <25190078+sudorand@users.noreply.github.com>
2025-07-15 11:14:29 -07:00
SpudGunMan
3f882dcfcd fix message.log
fixing issue for log in https://github.com/SpudGunMan/meshing-around/pull/161

Co-Authored-By: SudoRand <25190078+sudorand@users.noreply.github.com>
2025-07-15 09:41:34 -07:00
SpudGunMan
b146fd6f64 Revert "enhance sysinfo"
This reverts commit 8709e5aed5.
2025-07-14 22:55:41 -07:00
SpudGunMan
8709e5aed5 enhance sysinfo
ChUtil/Node value
2025-07-14 22:13:49 -07:00
SpudGunMan
caf8a2708b Update log.py
fix time display
2025-07-14 22:04:22 -07:00
SpudGunMan
9b4200c198 Update config.template
adjustments for 2.7.2 firmware might change again
2025-07-14 21:54:30 -07:00
SudoRand
097cae6e94 Allow chunker to consolidate lines when possible
This allows the chunker to consolidate lines into significantly
fewer messages in many cases without exceeding the max chunk size.

Without this change, the chunker will either emit all lines in
one message (if it fits in a single chunk) or else each line will be in
a separate message. This often creates a long series of short messages,
which doesn't transmit as quickly or display as compact.

Instead, this consolidates as many lines as possible into each
message, while being sure to stay within the chunk size limit.
This should reduce the load on the mesh, and it's also more readable.
2025-07-14 12:27:14 -06:00
SpudGunMan
0a260b28b6 Update llm.py
last time?
2025-06-26 19:30:33 -07:00
SpudGunMan
3f5c6f2e9a Update llm.py 2025-06-26 19:03:13 -07:00
SpudGunMan
8a4f7a904a Update llm.py 2025-06-26 18:49:16 -07:00
SpudGunMan
0bc3d392cf fix Interface logic
a condition where TCP interfaces can fail leaving a none condition. this should resolve the errored interface better.
2025-06-25 07:30:46 -07:00
SpudGunMan
5eaef8b5b8 Update sysEnv.sh
enhance with git update check

Co-Authored-By: Johannes le Roux <dade@dade.co.za>
2025-06-25 07:25:04 -07:00
SpudGunMan
3a0007771d Update update.sh 2025-06-22 20:04:18 -07:00
SpudGunMan
67ba2b1fb5 Create update.sh 2025-06-22 19:59:31 -07:00
SpudGunMan
f2e7a9aa5c Update config.template 2025-06-18 12:15:44 -07:00
SpudGunMan
9d22270dde highfly_ignoreList
some nodes have bad altimeters
2025-06-18 12:14:14 -07:00
SpudGunMan
409d07436e Update settings.py 2025-06-10 11:22:32 -07:00
SpudGunMan
5ab0001f2b Update system.py
https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 20:32:28 -07:00
SpudGunMan
5e34537af7 fix
reference https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 19:26:51 -07:00
SpudGunMan
1764bdf4f3 enhance
the database page to include qrz.db
2025-06-07 21:05:49 -07:00
SpudGunMan
2290f07351 Update locationdata.py 2025-06-06 17:54:31 -07:00
SpudGunMan
ee01051cf7 Update config.template 2025-06-06 17:54:29 -07:00
Kelly
de50a52fa6 Merge pull request #153 from rfschmid/allow-dms-to-numeric-short-names 2025-06-05 19:38:44 -07:00
SpudGunMan
8eabfaa9c4 enhance ignore logic 2025-06-04 10:37:58 -07:00
Russell Schmidt
ca7114b058 Allow DMs to numeric short names 2025-06-04 11:55:30 -05:00
Kelly
8b94dc8111 Merge pull request #152 from SudoRand/main 2025-06-01 19:03:01 -07:00
SudoRand
5b26aabb00 Config for whether ollama responds to non-commands
The current behavior is that whenever ollama is enabled the LLM replies
to all non-command message. This setting allows limiting it the LLM to
run only in response to the "ask:" and "askai" commands.

Default is True to keep consistent with previous behavior.
2025-06-01 15:32:08 -06:00
SudoRand
67b3c67348 Fix bug that always enabled news_random_line_only
This looks like a simple typo that accidentally used read_news_enabled
for the news_random_line_only parameter. As a result, the
news_random_line_only setting was always treated as True (since this
line it only executed if read_news_enabled was True). Now it obeys the
configuration value.
2025-06-01 14:02:19 -06:00
SpudGunMan
860cceec59 Update locationdata.py 2025-05-28 15:20:33 -07:00
Kelly
53a0535e55 Merge pull request #149 from SpudGunMan/lab
High Altitude Alerts
2025-05-23 19:14:51 -07:00
SpudGunMan
621f4ad916 enhance 2025-05-23 19:10:15 -07:00
SpudGunMan
118857ec15 Update system.py 2025-05-21 16:52:01 -07:00
SpudGunMan
1be13be92a highfly enhancements 2025-05-21 16:43:56 -07:00
SpudGunMan
895fc3fd37 Update README.md 2025-05-21 16:36:55 -07:00
SpudGunMan
0e0bda60ad Update system.py 2025-05-21 16:36:52 -07:00
SpudGunMan
903767f4b3 Update settings.py 2025-05-21 16:36:46 -07:00
SpudGunMan
f54d362ea0 Update config.template 2025-05-21 16:36:43 -07:00
SpudGunMan
60bb68c6b5 Update system.py 2025-05-21 16:20:54 -07:00
SpudGunMan
feb9a1d9b3 FlightDetector
high alt detection
2025-05-21 16:18:46 -07:00
SpudGunMan
d055c35c96 Update install.sh 2025-05-11 13:07:52 -07:00
SpudGunMan
27820daaf4 Cron4W3 2025-05-10 15:34:33 -07:00
SpudGunMan
56e8e1c0d5 🐇🪵
config register set in config.ini for hop logs
2025-05-01 10:35:43 -07:00
SpudGunMan
4545b8f4a4 Update locationdata.py 2025-04-28 23:10:52 -07:00
SpudGunMan
6ed48d49ce Update videopoker.py
fix gameplay
2025-04-24 15:54:47 -07:00
SpudGunMan
a3a54b081d Update system.py 2025-04-23 19:34:13 -07:00
SpudGunMan
ab420af63e Update system.py 2025-04-23 09:24:51 -07:00
SpudGunMan
a55c61c47d Update web.py 2025-04-23 07:17:02 -07:00
SpudGunMan
7236f47eb7 Update README.md 2025-04-17 13:30:53 -07:00
SpudGunMan
05e11ae5f8 Update README.md 2025-04-13 10:04:15 -07:00
SpudGunMan
f8ffcc19b1 Update config.template 2025-04-13 10:04:13 -07:00
SpudGunMan
ea20eec604 Update README.md 2025-04-11 14:51:50 -07:00
SpudGunMan
d1204d2c26 ignoreEAS and USGS alert word
enhance with list to ignore words not wanted for broadcast
2025-04-11 14:32:51 -07:00
SpudGunMan
654d8b3ff7 Update README.md 2025-04-10 16:14:32 -07:00
SpudGunMan
3bf12d62b5 Update mesh_bot.py 2025-04-10 16:09:15 -07:00
SpudGunMan
0ec8613d27 Update install.sh
issues raised https://github.com/SpudGunMan/meshing-around/issues/139
2025-04-10 15:57:56 -07:00
SpudGunMan
10dd413ae7 Update config.template 2025-04-10 15:50:12 -07:00
SpudGunMan
09ac7525b3 Update mesh_bot.py 2025-04-10 15:48:47 -07:00
SpudGunMan
aac497dfa0 config.ini Scheduler enhancments
request from https://github.com/SpudGunMan/meshing-around/issues/141

enhances with a basic announcement from config.ini
2025-04-10 15:46:36 -07:00
SpudGunMan
6f652230b0 fixQRZ formatting
and enhance saving names without info packet
2025-04-04 12:00:09 -07:00
SpudGunMan
6f1c44e62a Update mesh_bot.py
enhance llm error
2025-04-02 19:36:12 -07:00
SpudGunMan
837d049acb Update locationdata.py 2025-03-30 14:00:14 -07:00
SpudGunMan
2463407ade Update system.py 2025-03-30 13:49:40 -07:00
SpudGunMan
af2bc7be0c enhance sysinfo 2025-03-30 13:45:22 -07:00
SpudGunMan
38654213e8 fix script run 2025-03-30 13:44:57 -07:00
SpudGunMan
a06819dbda enhance bbsack 2025-03-30 11:49:54 -07:00
SpudGunMan
9818cccbbf fix BBSLink for open mode
fix for issue raised https://github.com/SpudGunMan/meshing-around/discussions/142
2025-03-30 11:29:00 -07:00
SpudGunMan
239dbb8be0 Update config.template
typo
2025-03-28 10:46:29 -07:00
SpudGunMan
872a9601d0 Update system.py 2025-03-27 20:31:53 -07:00
SpudGunMan
2b6dc726e1 valert for USGS Volcano Data 2025-03-27 19:44:02 -07:00
SpudGunMan
ef27ddff84 Update locationdata.py 2025-03-27 19:32:54 -07:00
SpudGunMan
8a8ad961d5 USGS Alerts 2025-03-27 19:31:56 -07:00
SpudGunMan
a8b4362d3c enhance VolcanoAlert
prevent stale records from being rebroadcast
2025-03-27 18:22:13 -07:00
SpudGunMan
dc731ae237 USGS Volcano Alerts 2025-03-27 16:11:21 -07:00
SpudGunMan
d0d024d770 Update system.py 2025-03-27 09:43:36 -07:00
SpudGunMan
9b633502e6 Update mesh_bot.py 2025-03-20 12:14:26 -07:00
Kelly
ac1a007ba4 Merge pull request #140 from todd2982/patch-2
Update .gitignore
2025-03-17 16:03:41 -07:00
todd2982
09cf6f585c Update .gitignore
Ignore rotated logs, install notes, and qrz db.
2025-03-17 02:07:01 -05:00
SpudGunMan
916719f1c5 Update mesh_bot.py 2025-03-15 17:31:51 -07:00
SpudGunMan
11a6dc3cf0 UTF-8-4-Windows
Co-Authored-By: dj505 <dj505@users.noreply.github.com>
2025-03-15 17:31:44 -07:00
SpudGunMan
c160678e79 Update locationdata.py 2025-03-07 17:55:56 -08:00
SpudGunMan
0c9fd919ab Update system.py 2025-03-07 17:53:17 -08:00
SpudGunMan
e17dc79896 🐞Bugs
issue https://github.com/SpudGunMan/meshing-around/issues/138
2025-03-04 12:46:16 -08:00
SpudGunMan
06d6855d92 cmd bang
this will solve all the worlds problems
2025-02-26 20:16:53 -08:00
SpudGunMan
66f937a645 expand BBS Block
ignore node who is cantankerous from all commands
2025-02-25 19:18:56 -08:00
SpudGunMan
f4985b744a Update README.md
Co-Authored-By: mikecarper <135079168+mikecarper@users.noreply.github.com>
2025-02-23 20:33:26 -08:00
SpudGunMan
7ae6174f96 Update hamtest.py 2025-02-23 20:06:08 -08:00
SpudGunMan
d44fdd4462 Update hamtest.py 2025-02-23 20:03:12 -08:00
SpudGunMan
3dd6da4684 Update hamtest.py 2025-02-23 20:02:15 -08:00
SpudGunMan
a229b57964 Update README.md 2025-02-23 19:25:15 -08:00
SpudGunMan
5e045b6447 Update README.md 2025-02-23 19:24:52 -08:00
SpudGunMan
1e328d4f4d Update README.md 2025-02-23 19:20:21 -08:00
SpudGunMan
879d141844 Update mesh_bot.py 2025-02-23 19:14:32 -08:00
SpudGunMan
7daf8c4c33 Update README.md 2025-02-23 18:57:50 -08:00
SpudGunMan
3e6d1f5c6f Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-02-23 18:40:44 -08:00
SpudGunMan
32deea9e3b hamtest
a game of the FCC/ARRL Question Pools
2025-02-23 18:40:41 -08:00
Kelly
793fabcdb8 Merge pull request #136 from NomDeTom/main
change maxBuffer to 200
2025-02-23 13:41:02 -08:00
SpudGunMan
a7a710208a Update send-environment-metrics.py 2025-02-22 17:29:42 -08:00
Tom
41efbc6189 Update config.template
change maxBuffer to 200 by default, as this is the longest that the recent firmware allows.
2025-02-23 01:28:17 +00:00
SpudGunMan
f399190d3c hangman 2025-02-21 21:48:12 -08:00
SpudGunMan
5760c10534 enhanceHangmen
is it hang man or hang men.
2025-02-21 21:31:16 -08:00
SpudGunMan
9deb4a9436 Update hangman.py 2025-02-21 19:04:56 -08:00
SpudGunMan
1f348d963d Update hangman.py 2025-02-21 18:56:07 -08:00
Kelly
b35edf13c8 Merge pull request #134 from dadecoza/main
Hangman!
2025-02-21 18:54:03 -08:00
Johannes le Roux
37185b9f8b Update hangman.py 2025-02-20 23:45:16 +02:00
Johannes le Roux
4e25535ede party face 2025-02-20 23:22:53 +02:00
Johannes le Roux
4de2a36099 added hangman 2025-02-20 22:53:28 +02:00
Kelly
6c0d6fd343 Merge pull request #133 from SpudGunMan/lab
Lab Enhancments
2025-02-19 18:32:06 -08:00
SpudGunMan
abd865c918 ignoreListFema 2025-02-19 18:29:22 -08:00
SpudGunMan
82222addbe Update log.py
enhance with more windows compatibility

Co-Authored-By: dj505 <7433694+dj505@users.noreply.github.com>
2025-02-19 18:21:43 -08:00
SpudGunMan
7750ce468b Update README.md 2025-02-19 17:31:22 -08:00
SpudGunMan
135778d511 winPython
Thanks Discord dj505 request for windows support
2025-02-19 17:30:06 -08:00
SpudGunMan
c54df673c3 refactorValue 2025-02-17 19:40:30 -08:00
SpudGunMan
2fec08060f FEMAIgnore Enhancment 2025-02-17 19:37:19 -08:00
SpudGunMan
ce9af3c0d3 Update locationdata.py 2025-02-17 14:11:06 -08:00
SpudGunMan
217cd01d0a Update locationdata.py 2025-02-17 14:00:40 -08:00
SpudGunMan
8a6057995b Update locationdata.py 2025-02-17 14:00:05 -08:00
SpudGunMan
47e21dbaab Chunker Improvement
Adjusted how packets are split, ignoring .?! which can confound things. @NomDeTom
2025-02-17 10:21:38 -08:00
SpudGunMan
267f50c591 Update locationdata.py 2025-02-16 11:04:37 -08:00
SpudGunMan
0013a7bb74 Update locationdata.py 2025-02-16 11:01:47 -08:00
SpudGunMan
73fe8be432 Update locationdata.py 2025-02-16 11:00:54 -08:00
SpudGunMan
3d45195ae9 refactor NOAA forecast to the API from bScrape
I cleaned up the config.ini noaaforecastduration you may want to set yours to `noaaforecastduration = 3` not like it was before that was a goof
2025-02-16 10:53:04 -08:00
SpudGunMan
ff390cf470 fixLog
reference https://github.com/SpudGunMan/meshing-around/discussions/125
2025-02-05 19:09:16 -08:00
SpudGunMan
17d8cd1067 enhance 2025-02-05 18:09:44 -08:00
SpudGunMan
b9348c906d enhance
better path handling
setting for IP Address

per https://github.com/SpudGunMan/meshing-around/issues/126

Co-Authored-By: mikecarper <135079168+mikecarper@users.noreply.github.com>
2025-02-05 18:09:08 -08:00
SpudGunMan
6ba3508cc5 outsideUSA rlist fix
@g7kse thanks for help on this

resolving https://github.com/SpudGunMan/meshing-around/issues/123
2025-02-04 19:04:08 -08:00
SpudGunMan
1c78f154da fixGameDisable Issue
from @PiHiker thanks for pointing out!
https://github.com/SpudGunMan/meshing-around/issues/124

closed issue
2025-02-04 18:37:50 -08:00
SpudGunMan
e0a3d0f94e Update system.py 2025-02-01 11:34:31 -08:00
SpudGunMan
066211e9f2 Update mesh_bot.py 2025-02-01 11:29:28 -08:00
SpudGunMan
5701cd108b Update qrz.py 2025-02-01 10:11:40 -08:00
SpudGunMan
b877a294ac Update install.sh 2025-02-01 09:19:49 -08:00
SpudGunMan
2aedcfc46e Update system.py 2025-02-01 09:04:48 -08:00
SpudGunMan
12147db5d0 Update mesh_bot.py 2025-01-31 22:06:31 -08:00
SpudGunMan
cef37b574b Update mesh_bot.py 2025-01-31 22:05:53 -08:00
SpudGunMan
6f121b7aac enhance QRZ
default to training mode, a new mode
2025-01-31 22:04:03 -08:00
SpudGunMan
9e31b7f47e deepseek compatibility
deepseek
2025-01-29 20:01:28 -08:00
SpudGunMan
f3103984ef Update README.md 2025-01-28 20:38:58 -08:00
SpudGunMan
9c8b3f0a54 Update CONTRIBUTING.md 2025-01-28 20:32:51 -08:00
SpudGunMan
f88cbf210e Update README.md 2025-01-28 20:30:30 -08:00
SpudGunMan
9909113beb Update README.md 2025-01-28 20:24:35 -08:00
SpudGunMan
c1b783b1cd Create README.md 2025-01-28 20:21:39 -08:00
SpudGunMan
9b3b6a5d3d Update README.md 2025-01-28 19:53:26 -08:00
SpudGunMan
cffdb3c089 Update README.md 2025-01-28 19:48:19 -08:00
SpudGunMan
7bb9c9ac55 Update README.md 2025-01-28 19:46:25 -08:00
SpudGunMan
830ec95080 🐛 2025-01-23 20:50:03 -08:00
SpudGunMan
0ea575ac70 Update README.md 2025-01-23 20:36:24 -08:00
SpudGunMan
d836255716 Update globalalert.py 2025-01-23 17:49:04 -08:00
SpudGunMan
4f115c9c21 Update pong_bot.py 2025-01-22 22:02:42 -08:00
SpudGunMan
63bd5b836d HELP
H
E
L
P
2025-01-22 22:00:56 -08:00
SpudGunMan
5ad9b9a261 Update mesh_bot.py 2025-01-22 21:51:04 -08:00
SpudGunMan
7a024b681f Create send-environment-metrics.py 2025-01-22 21:21:35 -08:00
SpudGunMan
75df5a695b Update mesh_bot.py 2025-01-21 21:39:17 -08:00
Kelly
0ef8cffd56 Merge pull request #119 from SpudGunMan/lab
LabCleanup
2025-01-21 20:26:09 -08:00
SpudGunMan
73e8e063d2 Update mesh_bot.py 2025-01-21 20:22:25 -08:00
SpudGunMan
82880677f4 Update mesh_bot.py 2025-01-21 20:21:32 -08:00
SpudGunMan
fe8ba8aaf4 Update mesh_bot.py 2025-01-21 20:10:01 -08:00
SpudGunMan
cea9147745 Update mesh_bot.py 2025-01-21 20:05:11 -08:00
SpudGunMan
c1c68d4c10 Update mesh_bot.py 2025-01-21 20:02:58 -08:00
SpudGunMan
5fcd21680e Update install.sh 2025-01-21 19:29:27 -08:00
SpudGunMan
9e1356172f Update install.sh 2025-01-21 19:23:20 -08:00
SpudGunMan
de7fdfad11 Update install.sh 2025-01-21 19:20:49 -08:00
SpudGunMan
a87055874a Update mesh_bot.py 2025-01-20 21:02:43 -08:00
SpudGunMan
5c7433091d Update mesh_bot.py 2025-01-20 21:00:18 -08:00
SpudGunMan
f0ca818461 Update checklist.py 2025-01-20 11:27:25 -08:00
SpudGunMan
76006dcda7 reverse_in_out 2025-01-20 10:54:51 -08:00
SpudGunMan
33abe646ae Update README.md 2025-01-19 12:09:55 -08:00
SpudGunMan
c47004c47c Update README.md 2025-01-19 12:09:25 -08:00
SpudGunMan
e66d945be7 Update checklist.py 2025-01-19 11:41:15 -08:00
SpudGunMan
10afc128f4 Update checklist.py 2025-01-19 11:35:15 -08:00
SpudGunMan
e6fc794951 Update requirements.txt 2025-01-19 11:07:34 -08:00
SpudGunMan
4839e9ba03 Update requirements.txt 2025-01-18 20:57:26 -08:00
SpudGunMan
bde15e311a Update README.md 2025-01-18 20:55:23 -08:00
SpudGunMan
21c83222e9 Update mesh_bot.py 2025-01-18 20:52:45 -08:00
SpudGunMan
bbcdd6656a Update README.md 2025-01-18 20:38:50 -08:00
SpudGunMan
7f61b86252 Update README.md 2025-01-18 20:10:19 -08:00
SpudGunMan
25ae27a162 Update system.py 2025-01-18 20:10:16 -08:00
SpudGunMan
a04133e82f Update README.md 2025-01-18 19:59:07 -08:00
SpudGunMan
2a9dfc90ee Update checklist.py 2025-01-18 18:09:42 -08:00
SpudGunMan
f1bf84f6f0 enhance 2025-01-18 18:08:36 -08:00
SpudGunMan
4b91ef10b4 Update README.md 2025-01-18 16:59:08 -08:00
SpudGunMan
cd4497b129 Update config.template 2025-01-18 16:28:52 -08:00
SpudGunMan
01374a8307 Update config.template 2025-01-18 16:28:40 -08:00
SpudGunMan
46c115b783 Update README.md 2025-01-18 16:27:04 -08:00
SpudGunMan
eec7230a84 fix 2025-01-18 16:24:04 -08:00
SpudGunMan
9394fd6ca9 qrzHello
says hello to new seen nodes
2025-01-18 16:22:35 -08:00
SpudGunMan
c6653da1f3 fixQRZ 2025-01-18 16:17:29 -08:00
SpudGunMan
9f47958a03 Update checklist.py 2025-01-18 16:14:00 -08:00
SpudGunMan
78e51b7be1 Update qrz.py 2025-01-18 16:06:40 -08:00
SpudGunMan
26fcf6fc02 enhance 2025-01-18 15:54:27 -08:00
SpudGunMan
c2336850fe Update checklist.py 2025-01-18 15:35:03 -08:00
SpudGunMan
54e0d17e70 Update checklist.py 2025-01-18 15:17:18 -08:00
SpudGunMan
7a6d1f7b29 Update checklist.py 2025-01-18 15:13:34 -08:00
SpudGunMan
7e26d3f0e5 Update README.md 2025-01-18 15:07:16 -08:00
SpudGunMan
89be8e13a2 Update README.md 2025-01-18 14:39:21 -08:00
SpudGunMan
aa8482ab52 Update config.template 2025-01-18 14:34:36 -08:00
SpudGunMan
69605e0984 Update checklist.py 2025-01-18 14:33:08 -08:00
SpudGunMan
8e15a3fc99 Update checklist.py 2025-01-18 14:31:48 -08:00
SpudGunMan
d671b19bce Update checklist.py 2025-01-18 14:27:09 -08:00
SpudGunMan
943dd4d5a3 enhanceChecklist 2025-01-18 14:26:10 -08:00
SpudGunMan
05d8671b3f Update checklist.py 2025-01-18 14:05:11 -08:00
SpudGunMan
4bccd33827 Update checklist.py 2025-01-18 14:03:26 -08:00
SpudGunMan
71ebe7087f Update mesh_bot.py 2025-01-18 14:01:23 -08:00
SpudGunMan
8dbffe2e63 enhance 2025-01-18 14:01:14 -08:00
SpudGunMan
cbea9b5294 enhanceNewIdeas
work on https://github.com/SpudGunMan/meshing-around/discussions/94
2025-01-18 13:31:54 -08:00
SpudGunMan
acdc94cd06 Create qrz.py 2025-01-18 12:35:11 -08:00
SpudGunMan
e9deb62047 Create checklist.py 2025-01-18 12:35:09 -08:00
SpudGunMan
f1ad470f88 Update README.md 2025-01-12 22:17:59 -08:00
SpudGunMan
b19f7be0b0 Update README.md 2025-01-12 21:57:59 -08:00
SpudGunMan
053acd1ac6 Update README.md 2025-01-12 21:56:45 -08:00
SpudGunMan
3d5b671d81 Update README.md 2025-01-12 21:55:52 -08:00
SpudGunMan
f090230c96 typo 2025-01-12 14:02:52 -08:00
SpudGunMan
d9040a4ec7 Update docker-terminal.bat 2025-01-12 13:52:05 -08:00
SpudGunMan
e35c954e5d fixNINAalerts 2025-01-12 13:45:26 -08:00
SpudGunMan
93ed84fdee Update README.md 2025-01-12 13:41:46 -08:00
Kelly
9f074e5250 Merge pull request #112 from SpudGunMan/lab
DE NINA Alerts
2025-01-12 13:36:09 -08:00
SpudGunMan
12d94fb0dc NINA alerts
@sodoku 👀 branch for testing new alerts
2025-01-12 13:22:30 -08:00
Kelly
afa2bc4024 Merge pull request #111 from sodoku/main
enable NINA alerts for Germany
2025-01-12 13:11:16 -08:00
Kelly
8dcbf66618 Merge pull request #108 from SpudGunMan/lab
Enhancement from Labwork
2025-01-12 13:09:14 -08:00
SpudGunMan
902b4f22ee readme 2025-01-12 12:43:44 -08:00
SpudGunMan
7ae0d5e927 Update pong_bot.py 2025-01-12 12:39:37 -08:00
SpudGunMan
49b8206e76 Update pong_bot.py 2025-01-12 12:36:08 -08:00
SpudGunMan
5a30cc7511 Update system.py 2025-01-12 12:16:08 -08:00
SpudGunMan
a85cc8c593 Update system.py 2025-01-12 12:09:51 -08:00
SpudGunMan
5ae496702d multiInterfaceRefactors 2025-01-12 11:59:48 -08:00
SpudGunMan
1dffa0987d Update settings.py 2025-01-12 11:47:57 -08:00
SpudGunMan
f3d07eed97 Update README.md 2025-01-12 11:27:29 -08:00
SpudGunMan
de8266b955 Update README.md 2025-01-12 11:19:36 -08:00
SpudGunMan
d482f2ccc9 docker enhancements 2025-01-12 11:19:27 -08:00
SpudGunMan
9f676a4c8d Update entrypoint.sh 2025-01-12 11:07:42 -08:00
SpudGunMan
5d0dae236c Update Dockerfile 2025-01-12 11:03:41 -08:00
SpudGunMan
bf32eca47d Update Dockerfile 2025-01-12 10:45:33 -08:00
SpudGunMan
dcef6da5bc Update Dockerfile 2025-01-12 10:36:34 -08:00
SpudGunMan
a1ffc8b1f6 Update Dockerfile 2025-01-12 10:21:14 -08:00
SpudGunMan
921b66f9e1 Update entrypoint.sh 2025-01-12 10:12:06 -08:00
SpudGunMan
0553a43a01 Update Dockerfile 2025-01-12 10:10:48 -08:00
sodoku
5079c67f62 enable NINA alerts for Germany 2025-01-12 13:26:34 +01:00
SpudGunMan
785deb2add add uninstall info
@noon92 👀
2025-01-11 10:09:55 -08:00
SpudGunMan
4b0654971c downgrade this log 2025-01-08 21:51:52 -08:00
SpudGunMan
d2fd133743 extraLocation
@turnrye another thing to check out
2025-01-05 21:50:36 -08:00
SpudGunMan
d689495ee7 Cleanup scripts
note here https://github.com/SpudGunMan/meshing-around/pull/103 and @turnrye can you review this branch and commit
2025-01-05 21:40:20 -08:00
SpudGunMan
b16b4e3c12 Update runShell.sh 2025-01-05 21:35:19 -08:00
SpudGunMan
10109672a7 Update sysEnv.sh 2025-01-05 21:34:05 -08:00
SpudGunMan
4a3cd2560c labCleanupDone 2025-01-05 21:27:25 -08:00
Kelly
576898b8fe Merge pull request #107 from turnrye/docker-compose
Docker compose enhancments
2025-01-05 21:16:41 -08:00
Kelly
4db9c136d6 Lab Cleanup
cleanLab
2025-01-05 21:15:13 -08:00
Kelly
a1a4c1b0f0 Merge branch 'lab2' into lab 2025-01-05 21:14:55 -08:00
Kelly
7b1b435e45 Merge branch 'lab' into docker-compose 2025-01-05 21:06:07 -08:00
SpudGunMan
54e716d2cc enhanceMultiNodeTelemetry 2025-01-05 20:53:30 -08:00
SpudGunMan
b44fa22c11 Update web.py 2025-01-05 20:20:34 -08:00
SpudGunMan
5829cdcef9 reportingEnhance 2025-01-05 20:18:02 -08:00
SpudGunMan
f0a93b0191 Update system.py 2025-01-05 18:24:11 -08:00
SpudGunMan
9014a7e8f9 Update system.py 2025-01-05 18:13:38 -08:00
SpudGunMan
6c9f9f2521 Update config.template 2025-01-05 18:11:57 -08:00
SpudGunMan
9bae30bcb1 Update config.template 2025-01-05 17:42:29 -08:00
SpudGunMan
7069ba1f43 Update system.py 2025-01-05 17:29:00 -08:00
SpudGunMan
ae844f8ecd Update system.py 2025-01-05 17:05:04 -08:00
SpudGunMan
af734ccb1f enhanceSentry 2025-01-05 17:01:07 -08:00
SpudGunMan
1ff5895bad reporting server
@g7kse check this out
2025-01-05 16:39:00 -08:00
SpudGunMan
f12fa0fe9b enhance 2025-01-05 16:20:17 -08:00
SpudGunMan
45c67024e7 enhanceSpotter 2025-01-05 16:19:59 -08:00
SpudGunMan
725cbd8045 Update locationdata.py 2025-01-05 16:01:12 -08:00
SpudGunMan
502a4f2666 Update locationdata.py 2025-01-05 15:37:41 -08:00
SpudGunMan
9aaebaad62 Update locationdata.py 2025-01-05 15:36:37 -08:00
SpudGunMan
d163bffba6 Update locationdata.py 2025-01-05 15:35:22 -08:00
SpudGunMan
36ba04a234 Update locationdata.py 2025-01-05 15:33:24 -08:00
SpudGunMan
0ac683b5c0 Update locationdata.py 2025-01-05 15:33:03 -08:00
SpudGunMan
b16d9322e3 Update system.py 2025-01-05 15:21:20 -08:00
SpudGunMan
868009b650 Update system.py 2025-01-05 15:07:54 -08:00
SpudGunMan
f917df709c refactorWatchDog 2025-01-05 14:58:48 -08:00
SpudGunMan
ab54dc06d7 enhance 2025-01-05 14:02:30 -08:00
SpudGunMan
c7b7b182b9 Update system.py 2025-01-05 13:36:24 -08:00
SpudGunMan
b78cf4d022 Update system.py 2025-01-05 13:18:21 -08:00
SpudGunMan
6f492ef382 interface Expansion 2025-01-05 13:15:54 -08:00
SpudGunMan
e24c9a9d56 Update install.sh 2025-01-05 11:49:37 -08:00
SpudGunMan
b1155dea7d Update install.sh 2025-01-04 21:34:28 -08:00
SpudGunMan
0d9245d448 Update install.sh 2025-01-04 18:49:37 -08:00
SpudGunMan
858bef7703 enhance 2025-01-04 18:48:20 -08:00
Ryan Turner
acf39d0870 fixup! fixup! fixup! fixup! fixup! Initial checkin 2025-01-04 20:40:27 -06:00
Ryan Turner
89a0884600 fixup! fixup! fixup! fixup! Initial checkin 2025-01-04 20:22:40 -06:00
Ryan Turner
70e11117f1 fixup! fixup! fixup! Initial checkin 2025-01-04 20:18:35 -06:00
Ryan Turner
d3f07ae524 fixup! fixup! Initial checkin 2025-01-04 19:58:12 -06:00
Ryan Turner
4f9c36fdad fixup! Initial checkin 2025-01-04 19:41:41 -06:00
Ryan Turner
df15fb54b0 Initial checkin 2025-01-04 19:39:23 -06:00
SpudGunMan
638dc4df16 Update install.sh 2025-01-04 12:54:45 -08:00
SpudGunMan
81e91ab6c5 Update install.sh 2025-01-04 12:53:50 -08:00
SpudGunMan
05476c2bff Update install.sh 2025-01-04 12:51:04 -08:00
SpudGunMan
3b4b0e8c32 Update install.sh 2025-01-04 00:04:57 -08:00
SpudGunMan
772218d108 Update install.sh 2025-01-04 00:04:28 -08:00
SpudGunMan
dae2e4c4f4 enhance embedded 2025-01-03 23:48:44 -08:00
SpudGunMan
5d5595ef8b Update install.sh 2025-01-03 23:42:00 -08:00
SpudGunMan
cf16fc3db7 Update install.sh 2025-01-03 23:39:59 -08:00
SpudGunMan
70659c9c14 Update install.sh 2025-01-03 23:31:08 -08:00
SpudGunMan
b04368f852 location aware
@Ruledo thanks for the idea for this!
2025-01-03 23:11:03 -08:00
SpudGunMan
9e5285a845 Update install.sh 2025-01-03 22:48:41 -08:00
SpudGunMan
475d475e18 Update install.sh 2025-01-03 22:43:56 -08:00
SpudGunMan
2c4cfa9e81 Update install.sh 2025-01-03 22:40:15 -08:00
SpudGunMan
15d7f75507 femtofox butfix
@noon92 this fixes the problem you saw
2025-01-03 22:29:40 -08:00
SpudGunMan
30131bc6d5 Update install.sh 2025-01-02 22:24:42 -08:00
SpudGunMan
5373b61f83 enhance 2025-01-02 22:14:13 -08:00
SpudGunMan
7eb629676b Update install.sh 2025-01-02 22:11:49 -08:00
SpudGunMan
db9b89d0ac Update pong_bot.py 2025-01-02 22:08:59 -08:00
SpudGunMan
d7af337a63 enhance 2025-01-02 22:06:00 -08:00
SpudGunMan
e3c5eb6add logLevel in Config
sysloglevel = DEBUG in config.ini
2025-01-02 21:58:15 -08:00
SpudGunMan
b0e57e8aca cleanup Embedded 2025-01-02 21:57:53 -08:00
SpudGunMan
b4168214b6 #hints 2025-01-02 21:02:14 -08:00
SpudGunMan
7fa5928537 Update README.md 2025-01-02 20:27:50 -08:00
SpudGunMan
f12198b140 enhance 2025-01-02 20:23:04 -08:00
Kelly
0d44ffb635 Merge pull request #101 from joshbowyer/patch-1
Update install.sh enhance stability
2025-01-02 20:00:22 -08:00
joshbowyer
c11ebf1443 Update install.sh
changed if statements to handle user input better
2025-01-02 21:55:09 -06:00
SpudGunMan
b94a5ebd8d POSIX 2025-01-02 19:26:30 -08:00
SpudGunMan
3392d2d5a8 Update install.sh 2025-01-01 11:48:57 -08:00
SpudGunMan
1df3a7aaa2 enhance 2025-01-01 11:35:49 -08:00
SpudGunMan
9a11214208 fix alerting 2024-12-28 09:28:08 -08:00
SpudGunMan
0a4f101370 Update install.sh 2024-12-27 17:22:58 -08:00
SpudGunMan
5f3c32dc00 Update install.sh 2024-12-27 16:50:24 -08:00
SpudGunMan
74cb135c6c Update install.sh
enhance embedded
2024-12-27 16:26:00 -08:00
SpudGunMan
a20e520501 Update install.sh 2024-12-27 14:03:20 -08:00
SpudGunMan
23e0e4c6a0 Update install.sh 2024-12-27 14:03:03 -08:00
SpudGunMan
10918546d6 Update install.sh 2024-12-27 14:01:19 -08:00
SpudGunMan
cf16cc6606 Update install.sh 2024-12-27 13:58:31 -08:00
SpudGunMan
3b73b665d6 Update install.sh 2024-12-27 13:57:02 -08:00
SpudGunMan
993fd760af Update install.sh 2024-12-27 13:55:41 -08:00
SpudGunMan
a029334576 Update install.sh 2024-12-27 13:48:47 -08:00
SpudGunMan
eb8143f298 Update install.sh 2024-12-27 13:36:15 -08:00
SpudGunMan
c756b447ac Update install.sh 2024-12-27 10:28:01 -08:00
SpudGunMan
cef05e061c Update install.sh 2024-12-27 10:04:58 -08:00
SpudGunMan
c85d517b91 Update install.sh 2024-12-27 10:03:13 -08:00
SpudGunMan
170d1a6a45 Update install.sh 2024-12-27 09:52:01 -08:00
SpudGunMan
8d2313cfb1 Update install.sh 2024-12-27 09:49:54 -08:00
SpudGunMan
ed8636f5a5 Update config.template 2024-12-26 16:28:52 -08:00
SpudGunMan
b95d94f06f alertChange 2024-12-26 09:29:30 -08:00
SpudGunMan
f7cdf446bf Update system.py 2024-12-24 18:44:44 -08:00
SpudGunMan
28e8e2705a fix Keyerror 2024-12-24 18:43:11 -08:00
SpudGunMan
9bc6f6f661 Update system.py 2024-12-24 12:21:58 -08:00
SpudGunMan
2630310210 Update system.py 2024-12-24 11:29:54 -08:00
SpudGunMan
3fae42305c sysEnv enhance 2024-12-23 18:56:05 -08:00
SpudGunMan
9cc8dd7143 Update runShell.sh 2024-12-23 16:36:33 -08:00
SpudGunMan
7ffa9d5309 Update mesh_bot.py 2024-12-23 13:17:06 -08:00
SpudGunMan
30d2b996c0 Update filemon.py 2024-12-23 13:16:11 -08:00
SpudGunMan
49c098ef0b Update filemon.py 2024-12-23 13:15:53 -08:00
SpudGunMan
afa41c6ecd Update runShell.sh 2024-12-23 13:12:06 -08:00
SpudGunMan
8861179cb2 Update runShell.sh 2024-12-23 13:11:47 -08:00
SpudGunMan
f32ceb0383 Update runShell.sh 2024-12-23 13:11:09 -08:00
SpudGunMan
9a380964aa Update runShell.sh 2024-12-23 13:09:46 -08:00
SpudGunMan
180a8261ca enhance 2024-12-23 12:55:29 -08:00
SpudGunMan
0536657c8e Update config.template 2024-12-23 12:52:17 -08:00
Kelly
c5a2330dd1 Merge pull request #98 from SpudGunMan/lab
external bash script access
2024-12-23 12:24:54 -08:00
SpudGunMan
dc0b5be387 Update README.md 2024-12-23 12:22:23 -08:00
SpudGunMan
a1f43a5e94 Update runShell.sh 2024-12-23 12:19:40 -08:00
SpudGunMan
b05a817769 Update runShell.sh 2024-12-23 12:18:26 -08:00
SpudGunMan
f7187fdf27 Update runShell.sh 2024-12-23 12:16:05 -08:00
SpudGunMan
cca51d68dd Update locationdata.py 2024-12-23 12:10:03 -08:00
SpudGunMan
21804cc975 scriptingEnhancment 2024-12-23 12:08:28 -08:00
SpudGunMan
7a9ee27336 Update filemon.py 2024-12-23 02:55:57 -08:00
SpudGunMan
0c637226b2 Update config.template 2024-12-22 20:31:13 -08:00
SpudGunMan
555b14ddc0 enhance🐝 2024-12-22 02:55:10 -08:00
SpudGunMan
656c23c631 Update system.py 2024-12-22 01:16:09 -08:00
SpudGunMan
bb591257c9 Update README.md 2024-12-22 00:13:49 -08:00
SpudGunMan
364a5c5c67 🐝
the b can be for the movie or a bible or any other fun idea. be kind.
2024-12-22 00:11:35 -08:00
SpudGunMan
8cb05d38db Update system.py 2024-12-20 21:26:16 -08:00
SpudGunMan
f9fe13f322 Update system.py 2024-12-20 21:22:21 -08:00
Kelly
b8d33cc270 Merge pull request #97 from SpudGunMan/emergencyalert
EnhanceEmergencyAlert
2024-12-20 14:39:31 -08:00
SpudGunMan
a6ce9e9211 remove Numpy 2024-12-20 12:22:01 -08:00
SpudGunMan
60bdabdd1b embedded 2024-12-20 02:00:36 -08:00
SpudGunMan
9c5c2080cf Update locationdata_eu.py 2024-12-20 01:17:47 -08:00
SpudGunMan
8f758229cb Update install.sh 2024-12-20 00:38:59 -08:00
SpudGunMan
8ac9c53f1a enhance groupPing 2024-12-19 18:29:35 -08:00
SpudGunMan
98cbf5528c fixEmbedded 2024-12-19 17:46:26 -08:00
SpudGunMan
6296150677 Update pong_bot.py 2024-12-19 17:40:10 -08:00
SpudGunMan
13cb1e8df9 Update mesh_bot.py 2024-12-19 17:39:15 -08:00
SpudGunMan
e26e876ccf Update system.py 2024-12-19 17:21:33 -08:00
SpudGunMan
550b50f74e Update settings.py 2024-12-19 17:06:24 -08:00
SpudGunMan
ac5aa1a201 Update system.py 2024-12-19 17:03:53 -08:00
SpudGunMan
19700f54c5 Update system.py 2024-12-19 16:55:26 -08:00
SpudGunMan
7e5626cd30 Update system.py 2024-12-19 16:27:09 -08:00
SpudGunMan
c27b6ed8a1 enhanceEmergency Alerting 2024-12-19 16:18:38 -08:00
SpudGunMan
717181bcd0 Update locationdata_eu.py 2024-12-19 16:07:07 -08:00
SpudGunMan
4d5916df29 Update settings.py 2024-12-18 19:58:34 -08:00
SpudGunMan
93b7a1d613 enableGBalerts 2024-12-18 19:58:21 -08:00
SpudGunMan
35cc029984 Update README.md 2024-12-18 19:54:47 -08:00
SpudGunMan
589d44c152 Update locationdata_eu.py 2024-12-18 19:52:49 -08:00
SpudGunMan
06a14d875f enableUKalerts 2024-12-18 19:39:55 -08:00
SpudGunMan
454f823ad7 england.GovAlert 2024-12-18 19:33:11 -08:00
SpudGunMan
6974c4ef66 Update locationdata_eu.py 2024-12-18 19:24:33 -08:00
SpudGunMan
bd956dfebc locationEnhance 2024-12-18 15:09:38 -08:00
SpudGunMan
4aaac5ba49 Update README.md 2024-12-18 13:57:58 -08:00
SpudGunMan
2ae792dd8d Update README.md 2024-12-18 10:55:53 -08:00
SpudGunMan
ca033f024e enhanceNews
returns a random line from the file
2024-12-18 10:53:44 -08:00
SpudGunMan
ad11f787de Update locationdata_eu.py 2024-12-17 23:12:52 -08:00
SpudGunMan
e3d1607c86 enhance EU 2024-12-17 23:11:05 -08:00
SpudGunMan
b68461cbc8 move the moon 2024-12-17 22:57:14 -08:00
SpudGunMan
ddad35aa1e Update README.md 2024-12-17 22:42:15 -08:00
SpudGunMan
35f4aad6f8 riverFlow 2024-12-17 22:16:02 -08:00
SpudGunMan
f08f98e040 Update locationdata.py 2024-12-17 21:55:12 -08:00
SpudGunMan
467376d9c7 Update mesh_bot.py 2024-12-17 21:47:05 -08:00
SpudGunMan
1cbdc93632 riverFlowAlpha 2024-12-17 20:32:07 -08:00
SpudGunMan
2323015617 riverFlood 2024-12-17 20:06:16 -08:00
SpudGunMan
51de0dee8a riverFlow 2024-12-17 13:32:08 -08:00
SpudGunMan
b74c0ebd36 Update wx_meteo.py 2024-12-17 13:29:15 -08:00
SpudGunMan
0a4c54a5a2 Update locationdata.py 2024-12-17 12:14:00 -08:00
SpudGunMan
481809493c Update wx_meteo.py 2024-12-16 20:56:28 -08:00
SpudGunMan
c3914e0423 Update mesh_bot.py 2024-12-16 20:54:57 -08:00
SpudGunMan
ac40254bc4 refactor Openmeteo wx
eliminate requirement for modules and use requests native
2024-12-16 20:43:52 -08:00
SpudGunMan
b6540a1d20 🚨improve EAS duplicates 2024-12-16 09:05:59 -08:00
Kelly
87d29d123f Merge pull request #96 from todd2982/patch-1 2024-12-16 08:05:24 -08:00
todd2982
0aa6f8cc07 Patch CVE found in base python image
Patches the following CVE:
CVE-2024-6345
CVE-2023-5752
2024-12-16 08:21:01 -06:00
SpudGunMan
e2bb480f5f output fix femtofox
Python 3.10.12 had issues
2024-12-15 01:04:34 -08:00
SpudGunMan
920f951e47 Update Dockerfile 2024-12-14 23:00:18 -08:00
SpudGunMan
215fe76f2a CodeQLBadge 2024-12-13 23:27:14 -08:00
SpudGunMan
1740bbf666 Update install.sh 2024-12-13 21:35:10 -08:00
SpudGunMan
f9370d47b4 Update install.sh 2024-12-13 21:34:01 -08:00
SpudGunMan
91072cb47d Update install.sh 2024-12-13 21:29:09 -08:00
SpudGunMan
c30be37f02 femtofox 2024-12-13 21:27:35 -08:00
SpudGunMan
d51dadba04 Update install.sh 2024-12-13 21:20:57 -08:00
SpudGunMan
99c404f479 moveThisShakeThat 2024-12-13 20:12:40 -08:00
SpudGunMan
659ee2959c cleanup 2024-12-13 20:10:59 -08:00
SpudGunMan
1ac9f3b0d6 loop detector 2024-12-13 20:04:20 -08:00
SpudGunMan
d0dc737863 Update README.md 2024-12-13 14:57:14 -08:00
SpudGunMan
e438c82a11 enhance 2024-12-13 14:19:22 -08:00
SpudGunMan
9d7d4601dc Update system.py 2024-12-13 13:29:10 -08:00
SpudGunMan
fdd741446c Update system.py 2024-12-13 13:15:11 -08:00
SpudGunMan
fdbab1685f Update locationdata.py 2024-12-13 13:06:05 -08:00
SpudGunMan
ed0940b126 🧀 2024-12-13 13:03:41 -08:00
SpudGunMan
a087c7bb3a Update system.py 2024-12-13 13:02:06 -08:00
SpudGunMan
0439db2ec0 sysinfo
returns telemetry info
2024-12-13 12:59:12 -08:00
SpudGunMan
c1a5d4d336 Create gpio.py 2024-12-13 12:30:49 -08:00
SpudGunMan
eeffc6361a enhance@🏓 2024-12-13 11:45:50 -08:00
SpudGunMan
e2be3c20b7 enhance🏓 2024-12-13 10:30:18 -08:00
SpudGunMan
b43c21fc98 emergency responder block list 2024-12-13 10:22:33 -08:00
SpudGunMan
e115f33d47 pingEnhancments🏓
added autoPingInChannel = False
 # Allows auto-ping feature in a channel, False forces DM

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

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

12
.gitignore vendored
View File

@@ -8,7 +8,8 @@ config.ini
venv/
# logs
logs/*.log
logs/
install_notes.txt
# modified .service files
etc/*.service
@@ -18,3 +19,12 @@ __pycache__/
# rag data
data/rag/*
# qrz db
data/qrz.db
# fileMonitor test file
bee.txt
# .csv files
*.csv

View File

@@ -1 +1,2 @@
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
there is some ideas for adding code in modules/README.md

View File

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

594
README.md
View File

@@ -1,17 +1,25 @@
# Mesh Bot for Network Testing and BBS Activities
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
![Example Use](etc/pong-bot.jpg "Example Use")
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
### Intelligent Keyword Responder
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
- **New Node Hello**: Greet new nodes on the mesh with a hello message
### Dual Radio/Node Support
- **Simultaneous Monitoring**: Monitor two networks at the same time.
### Network Tools
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
- **Network Monitoring**: Alert on noisy nodes, node locations, and best placment for relay nodes.
### Multi Radio/Node Support
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
- **Flexible Messaging**: send mail and messages, between networks.
### Advanced Messaging Capabilities
@@ -19,67 +27,184 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach
- **BBS Linking**: Combine multiple bots to expand BBS reach.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
- **New Node Hello**: Send a hello to any new node seen in text message.
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **NOAA/USGS location Data**: Get localized weather(alerts), Earthquake, River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **Wiki Integration**: Look up data using Wikipedia results.
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
- **Satellite Pass Info**: Get passes for satellite at your location.
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
- **Voice/Command Triggers**: The following keywords can be used via voice (VOX) to trigger bot functions "Hey Chirpy!"
- Say "Hey Chirpy.."
- `joke`: Tells a joke
- `weather`: Returns local weather forecast
- `moon`: Returns moonrise/set and phase info
- `daylight`: Returns sunrise/sunset times
- `river`: Returns NOAA river flow info
- `tide`: Returns NOAA tide information
- `satellite`: Returns satellite pass info
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
### Fun and Games
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
- **Telemetry Leaderboard**: Fun stats like lowest 🪫 battery or coldest temp 🥶
#### QuizMaster
- **Interactive Group Quizzes**: The QuizMaster module allows admins to start and stop quiz games for groups. Players can join, leave, and answer questions directly via DM or channel.
- **Scoring and Leaderboards**: Players can check their scores and see the top performers with `q: score` and `q: top`.
- **Easy Participation**: Players answer questions by prefixing their answer with `q:`, e.g., `q: 42`.
#### Survey Module
- **Custom Surveys**: Easily create and deploy custom surveys by editing JSON files in `data/survey`. Multiple surveys can be managed (e.g., `survey snow`).
- **User Feedback Collection**: Users can participate in surveys via DM, and responses are logged for later review.
### Radio Frequency Monitoring
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
- **Speech to Text Brodcasting to Mesh** Using [vosk](https://alphacephei.com/vosk/models) to translate to text.
### NOAA EAS Alerts
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
### File Monitor Alerts
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
- **News File**: On request of news, the contents of the file are returned. Can also call multiple news sources or files.
- **Shell Command Access**: Pass commands via DM directly to the host OS with replay protection.
### Data Reporting
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
- **RSS and news feeds**: Get data in mesh from many sources!
### Robust Message Handling
- **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.
### Installation
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project. 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
### Quick Setup
#### Clone the Repository
If you dont have git you will need it `sudo apt-get install git`
```sh
git clone https://github.com/spudgunman/meshing-around
```
The code is under active development, so make sure to pull the latest changes regularly!
#### Optional Automation of setup
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
#### Docker Installation
If you prefer to use Docker, follow these steps:
## Full list of commands for the bot
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
2. Build the Docker image:
```sh
cd meshing-around
docker build -t meshing-around .
```
3. Run the Docker container:
```sh
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
```
### Networking
| Command | Description | ✅ Works Off-Grid |
|---------|-------------|-
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `leaderboard` | Shows extreme mesh metrics like lowest battery 🪫 `leaderboard reset` allows admin reset | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `sysinfo` | Returns the bot node telemetry info | ✅ |
| `test` | used to test the limits of data transfer (`test 4` sends data to the maxBuffer limit default 200 charcters) via DM only | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `echo` | Echo string back, disabled by default | ✅ |
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
#### Custom Install
### Radio Propagation & Weather Forecasting
| Command | Description | |
|---------|-------------|-------------------
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
| `earthquake` | Returns the largest and number of USGS events for the location | |
| `hfcond` | Returns a table of HF solar conditions | |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `riverflow` | Return information from NOAA for river flow info. | |
| `solar` | Gives an idea of the x-ray flux | |
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `tide` | Returns the local tides (NOAA data source) | |
| `valert` | Returns USGS Volcano Data | |
| `wx` | Return local weather forecast, NOAA or Open Meteo (which also has `wxc` for metric and imperial) | |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
| `mwx` | Return the NOAA Coastal Marine Forecast data | |
### Bulletin Board & Mail
| Command | Description | |
|---------|-------------|-
| `bbshelp` | Returns the following help message | ✅ |
| `bbslist` | Lists the messages by ID and subject | ✅ |
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
| `sms:` | Send sms-email to multiple address on file | |
| `setemail`| Sets the email for easy communications | |
| `setsms` | Adds the SMS-Email for quick communications | |
| `clearsms` | Clears all SMS-Emails on file for node | |
### Data Lookup
| Command | Description | |
|---------|-------------|-
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard on device, like Store and Forward, returns the PublicChannel and Current | ✅ |
| `readnews` | returns the contents of a file (data/news.txt, by default) can also `news mesh` via the chunker on air | ✅ |
| `readrss` | returns a set RSS feed on air | |
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
| `wiki:` | Searches Wikipedia (or local Kiwix server) and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `howfar` | returns the distance you have traveled since your last HowFar. `howfar reset` to start over | ✅ |
| `howtall` | returns height of something you give a shadow by using sun angle | ✅ |
### CheckList
| Command | Description | |
|---------|-------------|-
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
| `checklist` | Display the checklist database, with note | ✅ |
### Games (via DM only)
| Command | Description | |
|---------|-------------|-
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
| `hangman` | Plays the classic word guess game | ✅ |
| `joke` | Tells a joke | |
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `survey` | Issues out a survey to the user | ✅ |
| `quiz` | QuizMaster Bot `q: ?` for more | ✅ |
| `tic-tac-toe`| Plays the game classic game | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
#### QuizMaster
To use QuizMaster the bbs_admin_list is the QuizMaster, who can `q: start` and `q: stop` to start and stop the game, `q: broadcast <message>` to send a message to all players.
Players can `q: join` to join the game, `q: leave` to leave the game, `q: score` to see their score, and `q: top` to see the top 3 players.
To Answer a question, just type the answer prefixed with `q: <answer>`
#### Survey
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
## Other Install Options
### Docker Installation - handy for windows
See further info on the [docker.md](script/docker/README.md)
### Manual Install
Install the required dependencies using pip:
```sh
pip install -r requirements.txt
@@ -90,8 +215,10 @@ Copy the configuration template to `config.ini` and edit it to suit your needs:
cp config.template config.ini
```
### Configuration
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
### Configuration Guide
The following is documentation for the config.ini file
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
```sh
meshtastic --ble-scan
```
@@ -102,7 +229,7 @@ meshtastic --ble-scan
# config.ini
# type can be serial, tcp, or ble.
# port is the serial port to use; commented out will try to auto-detect
# hostname is the IP address of the device to connect to for TCP type
# hostname is the IP/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for BLE type
[interface]
@@ -117,27 +244,42 @@ enabled = False
```
### General Settings
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
```ini
[general]
respond_by_dm_only = True
defaultChannel = 0
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
cmdBang = False # require ! to be the first character in a command
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
```
### Location Settings
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
```ini
[location]
enabled = True
lat = 48.50
lon = -123.0
# To fuzz the location of the above
fuzzConfigLocation = True
# Fuzz all values in all data
fuzzItAll = False
UseMeteoWxAPI = True
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
coastalForecastDays = 3 # number of data points to return, default is 3
```
### Module Settings
Modules can be enabled or disabled as needed.
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
```ini
[bbs]
@@ -158,18 +300,93 @@ lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
### Sentry Settings
Sentry Bot detects anyone coming close to the bot-node.
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
```ini
SentryEnabled = True # detect anyone close to the bot
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
highFlyingAlert = True # HighFlying Node alert
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
```
### E-Mail / SMS Settings
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
```ini
[smtp]
# enable or disable the SMTP module, minimum required for outbound notifications
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
sysopEmails =
# See config.template for all the SMTP settings
SMTP_SERVER = smtp.gmail.com
SMTP_AUTH = True
EMAIL_SUBJECT = Meshtastic✉
```
### Emergency Response Handler
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
```ini
[emergencyHandler]
enabled = True # enable or disable the emergency response handler
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
alert_interface = 1
```
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS and NINA
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
```ini
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
ignoreFEMAenable = True # Ignore any headline that includes followig word list
ignoreFEMAwords = test,exercise
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# To use other country services enable only a single optional serivce
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
myRegionalKeysDE = 110000000000,120510000000
```
#### NOAA EAS
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
```ini
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
ignoreEASenable = True # Ignore any headline that includes followig word list
ignoreEASwords = test,advisory
```
#### USGS River flow data and Volcano alerts
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
Volcano Alerts use lat/long to determine ~1000km radius
```ini
[location]
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
riverList = 14144700 # example Mouth of Columbia River
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
```
### Repeater Settings
A repeater function for two different nodes and cross-posting messages. The [`repeater_channels`] is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
```ini
[repeater] # repeater module
@@ -177,12 +394,62 @@ enabled = True
repeater_channels = [2, 3]
```
### Ollama (LLM/AI) Settings
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
```ini
# Enable ollama LLM see more at https://ollama.com
ollama = True # Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
```
Also see `llm.py` for changing the defaults of:
```ini
# LLM System Variables
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
# Used in the meshBotAI template (legacy)
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
### Wikipedia Search Settings
The Wikipedia search module can use either the online Wikipedia API or a local Kiwix server for offline wiki access. Kiwix is especially useful for mesh networks operating in remote or offline environments.
```ini
# Enable or disable the wikipedia search module
wikipedia = True
# Use local Kiwix server instead of online Wikipedia
# Set to False to use online Wikipedia (default)
useKiwixServer = False
# Kiwix server URL (only used if useKiwixServer is True)
kiwixURL = http://127.0.0.1:8080
# Kiwix library name (e.g., wikipedia_en_100_nopic_2024-06)
# Find available libraries at https://library.kiwix.org/
kiwixLibraryName = wikipedia_en_100_nopic_2024-06
```
To set up a local Kiwix server:
1. Install Kiwix tools: https://kiwix.org/en/ `sudo apt install kiwix-tools -y`
2. Download a Wikipedia ZIM file to `data/`: https://library.kiwix.org/ `wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_nopic_2025-09.zim`
3. Run the server: `kiwix-serve --port 8080 wikipedia_en_100_nopic_2025-09.zim`
4. Set `useKiwixServer = True` in your config.ini
The bot will automatically extract and truncate content to fit Meshtastic's message size limits (~500 characters).
### 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
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
@@ -191,49 +458,34 @@ 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)
```ini
# Enable ollama LLM see more at https://ollama.com
ollama = True # Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma2 #ollamaModel = llama3.1
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
```
Also see `llm.py` for changing the defaults of:
```ini
# LLM System Variables
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
enabled = True
file_path = alert.txt
broadcastCh = 2,4
filemon_enabled = True
file_path = alert.txt # text file to monitor for changes
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
enable_read_news = False # news command will return the contents of a text file
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
allowXcmd = True
```
#### NOAA EAS
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
```ini
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2,4
```
#### Offline EAS
To Monitor EAS with no internet connection see the following notes
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
no examples yet for these tools
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
- this can be used with a rtl-sdr to capture alerts
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [dsame3](https://github.com/jamieden/dsame3)
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
@@ -245,8 +497,38 @@ The following example shell command will pipe rtl_sdr to alert.txt
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
#### Newspaper on mesh
Maintain multiple news sources. Each source should be a file named `{source}_news.txt` in the `data/` directory (for example, `data/mesh_news.txt`).
- To read the default news, use the `readnews` command (reads from `data/news.txt`.
- To read a specific source, use `readnews abc` to read from `data/abc_news.txt`.
This allows you to organize and access different news feeds or categories easily.
External scripts can update these files as needed, and the bot will serve the latest content on request.
### Greet new nodes QRZ module
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
```ini
[qrz]
enabled = True # QRZ Hello to new nodes
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
training = True # Training mode will not send the hello message to new nodes, use this to build up database
```
### Scheduler
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
In the config.ini enable the module
```ini
[scheduler]
enabled = False # enable or disable the scheduler module
interface = 1 # channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
interval = # interval to use when time is not set (e.g. every 2 days)
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
```
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
```python
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
@@ -257,112 +539,32 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
```
#### BBS Link
The scheduler also handles the BBL Link Brodcast message
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
```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
```
### Firmware 2.6 DM Key, and 2.7 CLIENT_BASE Favorite Nodes
Firmware 2.6 introduced [PKC](https://meshtastic.org/blog/introducing-new-public-key-cryptography-in-v2_5/), enabling secure private messaging by adding necessary keys to each node. To fully utilize this feature, you should add favorite nodes—such as BBS admins—to your nodes favorites list to ensure their keys are retained. A helper script is provided to simplify this process:
- Run the helper script from the main program directory: `python3 script/addFav.py`
- By default, this script adds nodes from `bbs_admin_list` and `bbslink_whitelist`
- If using a virtual environment, run: `launch.sh addfav`
To configure favorite nodes, add their numbers to your config file:
```conf
[general]
favoriteNodeList = # list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
```
### MQTT Notes
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. Tested working fully Firmware:2.6.11 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
### Requirements
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install geopy
pip install schedule
pip install wikipedia
```
For open-meteo use:
```sh
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
For the Ollama LLM:
```sh
pip install ollama
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
## 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) | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
### Radio Propagation & Weather Forcasting
| Command | Description | |
|---------|-------------|-------------------
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `solar` | Gives an idea of the x-ray flux | |
| `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 |
### Bulletin Board & Mail
| Command | Description | |
|---------|-------------|-
| `bbshelp` | Returns the following help message | ✅ |
| `bbslist` | Lists the messages by ID and subject | ✅ |
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
### Data Lookup
| Command | Description | |
|---------|-------------|-
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
### Games (via DM)
| Command | Description | |
|---------|-------------|-
| `joke` | Tells a joke | ✅ |
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
~~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~~
# Recognition
@@ -380,15 +582,59 @@ I used ideas and snippets from other responder bots and want to call them out!
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
- [Golf](https://github.com/danfriedman30/pythongame)
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
### Special Thanks
- **xdep**: For the reporting tools.
- **Nestpebble**: For new ideas and enhancements.
- **mrpatrick1991**: For Docker configurations.
- **Mike O'Connell/skrrt**: For [text](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
- **Josh**: For more bashing on installer!
- **dj505**: trying it on windows!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Iris**: testing and finding 🐞
- **FJRPiolt**: testing bugs out!!
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
### Requirements
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install schedule
pip install wikipedia
```
For the Ollama LLM:
```sh
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.

View File

@@ -1,91 +1,158 @@
#config.ini
# type can be serial, tcp, or ble
# port is the serial port to use, commented out will try to auto-detect
# hostname is the IP address of the device to connect to for tcp type
# hostname is the IP/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for ble type
[interface]
type = serial
port = /dev/ttyACM0
# port = /dev/ttyUSB0
# port = COM1
# hostname = 192.168.0.1
# hostname = meshtastic.local
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support
# Additional interface for multi radio support
[interface2]
enabled = False
type = serial
port = /dev/ttyUSB0
#port = /dev/ttyACM1
# port = /dev/ttyACM1
# port = COM1
# hostname = meshtastic.local
# hostname = localhost
# mac = 00:11:22:33:44:55
# example, the third interface would be [interface3] up to 9
[general]
# if False will respond on all channels but the default channel
respond_by_dm_only = True
# Allows auto-ping feature in a channel, False forces to 1 ping only
autoPingInChannel = False
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
defaultChannel = 0
# ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreDefaultChannel = False
# ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
ignoreChannels =
# require ! to be the first character in a command
cmdBang = False
# require explicit command, the message will only be processed if it starts with a command word
explicitCmd = True
# list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
favoriteNodeList =
# motd is reset to this value on boot
motd = Thanks for using MeshBOT! Have a good day!
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 RSS module, and truncate the story
rssEnable = True
rssFeedURL = http://www.hackaday.com/rss.xml,http://rss.slashdot.org/Slashdot/slashdotMain
# RSS feed names must match the order of the URLs above, default is used if no match
rssFeedNames = default,slashdot
rssMaxItems = 3
rssTruncate = 100
# enable or disable the wikipedia search module
wikipedia = True
# Use local Kiwix server instead of online Wikipedia
# Set to False to use online Wikipedia, or provide Kiwix server URL
useKiwixServer = False
# Kiwix server URL (e.g., http://127.0.0.1:8080)
kiwixURL = http://127.0.0.1:8080
# Kiwix library name (e.g., wikipedia_en_100_nopic_2025-09)
kiwixLibraryName = wikipedia_en_100_nopic_2025-09
# Enable ollama LLM see more at https://ollama.com
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# Ollama model to use (defaults to gemma3:270m)
# ollamaModel = gemma3:latest
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# Produce LLM replies to messages that aren't commands?
# If False, the LLM only replies to the "ask:" and "askai" commands.
llmReplyToNonCommands = True
# if True, the input is sent raw to the LLM, if False uses legacy template query
rawLLMQuery = True
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
reverseSF = False
# history command
enableCmdHistory = True
# command history ignore list ex: 2813308004,4258675309
lheardCmdIgnoreNodes =
# 24 hour clock
zuluTime = False
# wait time for URL requests
urlTimeout = 10
urlTimeout = 15
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
mastermind = True
golfsim = True
#Do not retry enabling interface if it fails, just exit to let OS restart the bot
dont_retry_disconnect = False
#echo command, will echo back your message as the bot
enableEcho = False
# command will only echo 1:1 if sent on this channel, otherwise it will prepend @yourname
echoChannel = 9
[emergencyHandler]
# enable or disable the emergency response handler
enabled = False
# 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
reqLocationEnabled = False
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
# channel to send a message to when the watchdog is triggered
SentryChannel = 9
# device interface and channel to send the alert message to
SentryInterface = 1
SentryChannel = 2
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
sentryIgnoreList =
# Enable detection sensor alert, requires external sensor connected to node
detectionSensorAlert = False
# HighFlying Node alert
highFlyingAlert = True
# Altitude in meters to trigger the alert
highFlyingAlertAltitude = 2000
# check with OpenSkyNetwork if highfly detected for aircraft
highflyOpenskynetwork = True
# Channel to send Alert when the high flying node is detected
highFlyingAlertInterface = 1
# to disable OTA alert set to unused channel like 9
highFlyingAlertChannel = 2
# list of nodes numbers to ignore high flying alert ex: 2813308004,4258675309
highFlyingIgnoreList =
[bbs]
enabled = True
@@ -93,26 +160,103 @@ 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 =
# enable API script access (increases disk i/o)
bbsAPI_enabled = False
# location module
[location]
enabled = True
lat = 48.50
lon = -123.0
# NOAA weather forecast days, the first two rows are today and tonight
NOAAforecastDuration = 4
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
fuzzConfigLocation = True
fuzzItAll = False
# Default to metric units rather than imperial
useMetric = False
# repeaterList lookup location (rbook / artsci)
# repeaterList lookup location (rbook / artsci / False)
repeaterLookup = rbook
# EAS Alert Broadcast
# NOAA weather forecast days
NOAAforecastDuration = 3
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
coastalEnabled = False
# Find the correct costal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
# pz = Puget Sound, ph = Honolulu HI, gm = Florida Keys, pk = Alaska
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
# myCoastalZone is the .txt file with the forecast data
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt
# number of data points to return, default is 3
coastalForecastDays = 3
# NOAA USGS Hydrology river identifiers, LID or USGS ID https://waterdata.usgs.gov 12484500 Columbia River at The Dalles, OR
# for multiple rivers use comma separated list e.g. 12484500,14105700
riverList =
# NOAA EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# Enable Ignore any message that includes following word list
ignoreEASenable = False
ignoreEASwords = test,advisory
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# Add extra location to the weather alert
enableExtraLocationWx = False
# Goverment Alert Broadcast defaults to FEMA IPAWS
eAlertBroadcastEnabled = False
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# Goverment Alert Broadcast Channels
eAlertBroadcastCh = 2
# Enable Ignore, headline that includes following word list
ignoreFEMAenable = True
ignoreFEMAwords = test,exercise
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
# Enable Ignore any message that includes following word list
ignoreUSGSEnable = False
ignoreUSGSWords = test,advisory
# Use DE Alert Broadcast Data
enableDEalerts = False
# comma separated list of regional codes trigger local alert.
# find your regional codet at https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json
myRegionalKeysDE = 110000000000,120510000000
# Satalite Pass Prediction
# Register for free API https://www.n2yo.com/login/
n2yoAPIKey =
# NORAD list https://www.n2yo.com/satellites/
satList = 25544,7530
# CheckList Checkin/Checkout
[checklist]
enabled = False
checklist_db = data/checklist.db
reverse_in_out = False
[qrz]
# QRZ Hello to new nodes with message
enabled = False
qrz_db = data/qrz.db
qrz_hello_string = "MeshBot says Hello! DM for more info."
# Training mode will not send the hello message to new nodes
training = True
# repeater module
[repeater]
@@ -121,13 +265,34 @@ enabled = False
# and rebroadcasted on the same channel on the other device/node/interface
# with great power comes great responsibility, danger could be lurking in use of this feature
# if you have the two nodes on the same radio configurations, you could create a feedback loop
repeater_channels =
repeater_channels =
[scheduler]
# enable or disable the scheduler module
enabled = False
# interface to send the message to
interface = 1
# channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
# enable overides the above and uses the motd as the message
schedulerMotd = False
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
# time of day in 24:00 hour format when value is 'day' and interval is not set
time =
[radioMon]
# using Hamlib rig control will monitor and alert on channel use
enabled = False
rigControlServerAddress = localhost:4532
# broadcast to all nodes on the channel can also be = 2,3
# device interface to send the message to
sigWatchBroadcastInterface = 1
# broadcast channel can also be a comma separated list of channels
sigWatchBroadcastCh = 2
# minimum SNR as reported by radio via hamlib
signalDetectionThreshold = -10
@@ -136,17 +301,113 @@ signalHoldTime = 10
# the following are combined to reset the monitor
signalCooldown = 5
signalCycleLimit = 5
# enable VOX detection using default input
voxDetectionEnabled = False
# description to use in the alert message
voxDescription = VOX
useLocalVoxModel = False
voxLanguage = en-us
voxInputDevice = default
voxOnTrapList = True
voxTrapList = chirpy
voxEnableCmd = True
[fileMon]
enabled = False
filemon_enabled = False
# text file to monitor for changes
file_path = alert.txt
# channel to send the message to can be 2,3 multiple channels comma separated
broadcastCh = 2
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7
# 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 = 160
# news command will return the contents of a text file
enable_read_news = False
news_file_path = ../data/news.txt
# only return a single random line from the news file
news_random_line = False
# enable the use of exernal shell commands, this enables some data in `sysinfo`
enable_runShellCmd = False
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs
allowXcmd = False
# Enable 2 factor authentication for x: commands
2factor_enabled = True
# time in seconds to wait for the correct 2FA answer
2factor_timeout = 100
[smtp]
# enable or disable the SMTP module
enableSMTP = False
# enable or disable the IMAP module for inbound email
enableImap = False
# list of Sysop Emails seperate with commas
sysopEmails =
SMTP_SERVER = smtp.gmail.com
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
SMTP_PORT = 587
# Sender email: be mindful of public access, don't use your personal email
FROM_EMAIL = none@gmail.com
SMTP_AUTH = True
SMTP_USERNAME = none@gmail.com
SMTP_PASSWORD = none
EMAIL_SUBJECT = Meshtastic✉
# IMAP not implimented yet
IMAP_SERVER = imap.gmail.com
# 993 IMAP over TLS/SSL, 143 legacy IMAP
IMAP_PORT = 993
# IMAP login usually same as SMTP
IMAP_USERNAME = none@gmail.com
IMAP_PASSWORD = none
IMAP_FOLDER = inbox
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
disable_emojis = False
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
mastermind = True
golfsim = True
hangman = True
hamtest = True
tictactoe = True
# enable or disable the quiz game module questions are in data/quiz.json
quiz = False
# enable or disable the survey game module questions are in data/survey/*_survey.json
survey = False
# this is the default survey to use when command givcen, from data/survey/example_survey.json
defaultSurvey = example
# Whether to record user ID in responses
surveyRecordID=True
# Whether to record location on start of survey
surveyRecordLocation=True
[messagingSettings]
# delay in seconds for response to avoid message collision /throttling
responseDelay = 2.2
# delay in seconds for splits in messages to avoid message collision /throttling
splitDelay = 2.5
# message chunk size in charcters, chunkr allows exceeding by 3 characters
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing in bytes
maxBuffer = 200
#Enable Extra logging of Hop count data
enableHopLogs = False
# Noisy Node Telemetry Logging and packet threshold
noisyNodeLogging = False
noisyTelemetryLimit = 5
logMetaStats = True
# Enable detailed packet logging all packets
DEBUGpacket = False
# metaPacket detailed logging, the filter negates the port ID
debugMetadata = False
metadataFilter = TELEMETRY_APP,POSITION_APP

7226
data/hamradio/extra.json Normal file

File diff suppressed because it is too large Load Diff

5126
data/hamradio/general.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
data/mesh_news.txt Normal file
View File

@@ -0,0 +1 @@
Today in meshtastic you are looking at the coolest bot on the block.

1
data/news.txt Normal file
View File

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

16
data/quiz_questions.json Normal file
View File

@@ -0,0 +1,16 @@
[
{
"question": "Which RFband is commonly used by Meshtastic devices in US regions?",
"answers": ["2.4 GHz", "433 MHz", "900 MHz", "5.8 GHz"],
"correct": 2
},
{
"question": "Yogi the bear 🐻 likes what food?",
"answers": ["Picnic baskets", "Fish", "Burgers", "Hot dogs"],
"correct": 0
},
{
"question": "What is the password for the Meshtastic MQTT broker?",
"answer": "large4cats"
}
]

View File

@@ -0,0 +1,15 @@
[
{
"type": "multiple_choice",
"question": "How Did you hear about us?",
"options": ["Meshtastic", "Discord", "Friend", "Other"]
},
{
"type": "integer",
"question": "How many nodes do you own?"
},
{
"type": "text",
"question": "What feature would you like to see next?"
}
]

View File

@@ -0,0 +1,15 @@
[
{
"type": "multiple_choice",
"question": "How often do you experience snowfall in your area?",
"options": ["Never", "Rarely", "Sometimes", "Often", "Every winter"]
},
{
"type": "integer",
"question": "What was the deepest snowfall (in inches) you've measured at your location?"
},
{
"type": "text",
"question": "Describe any challenges you face during heavy snowfall."
}
]

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/mesh_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot.service
# sudo systemctl start mesh_bot.service
[Unit]

View File

@@ -1,6 +1,7 @@
# /etc/systemd/system/mesh_bot.service
# /etc/systemd/system/mesh_bot_reporting.service
# sudo systemctl daemon-reload
# sudo systemctl start mesh_bot.service
# sudo systemctl enable mesh_bot_reporting.service
# sudo systemctl start mesh_bot_reporting.service
[Unit]
Description=MeshingAround-Reporting

23
etc/mesh_bot_w3.tmp Normal file
View File

@@ -0,0 +1,23 @@
# /etc/systemd/system/mesh_bot_w3.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot_w3.service
# sudo systemctl start mesh_bot_w3.service
[Unit]
Description=MeshingAround-W3Server
After=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/dir/
ExecStart=python3 modules/web.py
ExecStop=pkill -f mesh_bot_w3.py
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable pong_bot.service
# sudo systemctl start pong_bot.service
[Unit]

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:
@@ -350,7 +350,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -371,6 +372,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
# open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -425,7 +436,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -745,7 +757,7 @@ def generate_main_html(log_data, system_info):
"""
template = Template(html_template)
return template.safe_substitute(
date=datetime.now().strftime('%Y_%m_%d'),
date=datetime.now().strftime('%Y-%m-%d'),
command_data=json.dumps(log_data['command_counts']),
message_data=json.dumps(log_data['message_types']),
activity_data=json.dumps(log_data['hourly_activity']),
@@ -913,6 +925,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

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:
@@ -359,7 +359,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -380,6 +381,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
#open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -434,7 +445,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -1036,7 +1048,7 @@ options: {
template = Template(html_template)
return template.safe_substitute(
date=datetime.now().strftime('%Y_%m_%d'),
date=datetime.now().strftime('%Y-%m-%d'),
command_data=json.dumps(log_data['command_counts']),
message_data=json.dumps(log_data['message_types']),
activity_data=json.dumps(log_data['hourly_activity']),
@@ -1207,6 +1219,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

View File

@@ -0,0 +1,53 @@
# file name: send-environment-metrics.py
# https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/send-environment-metrics.py
from meshtastic.protobuf import portnums_pb2, telemetry_pb2
from meshtastic import BROADCAST_ADDR
import time
# For connection over serial
# import meshtastic.serial_interface
# interface = meshtastic.serial_interface.SerialInterface()
# For connection over TCP
import meshtastic.tcp_interface
interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=False)
# Create a telemetry data object
telemetry_data = telemetry_pb2.Telemetry()
telemetry_data.time = int(time.time())
#telemetry_data.local_stats.upTime = 0
telemetry_data.environment_metrics.temperature = 0
# telemetry_data.environment_metrics.voltage = 0
# telemetry_data.environment_metrics.current = 0
# telemetry_data.environment_metrics.relative_humidity = 0
# telemetry_data.environment_metrics.barometric_pressure = 0
# telemetry_data.environment_metrics.gas_resistance = 0
# telemetry_data.environment_metrics.iaq = 0
# telemetry_data.environment_metrics.distance = 0
# telemetry_data.environment_metrics.lux = 0
# telemetry_data.environment_metrics.white_lux = 0
# telemetry_data.environment_metrics.ir_lux = 0
# telemetry_data.environment_metrics.uv_lux = 0
# telemetry_data.environment_metrics.wind_direction = 0
# telemetry_data.environment_metrics.wind_speed = 0
# telemetry_data.environment_metrics.wind_gust = 0
# telemetry_data.environment_metrics.wind_lull = 0
# telemetry_data.environment_metrics.weight = 0
# Read the uptime
# with open('/proc/uptime', 'r') as uptime:
# telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
# Read the CPU temperature
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
telemetry_data.environment_metrics.temperature = int(cpu_temp.read()) / 1000
interface.sendData(
telemetry_data,
destinationId=BROADCAST_ADDR,
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
wantResponse=False,
)
interface.close()

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
@@ -9,6 +9,7 @@ projectName = "example_handler" # name of _handler function to match the functio
randomNode = False # Set to True to use random node IDs
# bot.py Simulated functions
deviceID = 1 # represents the device/node number
def get_NodeID():
nodeList = [4258675309, 1212121212, 1234567890, 9876543210]
if randomNode:
@@ -16,22 +17,43 @@ def get_NodeID():
else:
nodeID = nodeList[0]
return nodeID
nodeID = get_NodeID() # assign a nodeID
def get_name_from_number(nodeID, length='short', interface=1):
# return random name for nodeID
names = ["Max","Molly","Jake","Kelly"]
return names[nodeID % len(names)]
#simulate GPS locations for testing
locations = [
(48.200909, -123.25719),
(48.330283,-123.260703),
(48.342735,-122.987911),
(48.205591,-122.998448)
]
lat, lon = random.choice(locations) # pick a random location
location = f"{lat},{lon}"
# # end Initialization of the tool
# # Function to handle, or the project in test
#from modules.llm import * # Import the LLM module
# # Project handler function code here
# example handler function canada()
def example_handler(message, nodeID, deviceID):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "
msg += f" Its: {readableTime} "
msg += f" You just sent: {message}"
return msg
if message != "":
# put code in test here
msg = f"Hello {get_name_from_number(nodeID)}, simulator ready for testing {projectName} project! on device {deviceID}"
msg += f" Your location is {location}"
msg += f" you said: {message}"
return msg
# # end of function test code
@@ -42,7 +64,7 @@ if __name__ == '__main__': # represents the bot's main loop
nodeInt = 1 # represents the device/node number
logger.info(f"System: Meshing-Around Simulator Starting for {projectName}")
nodeID = get_NodeID() # assign a nodeID
projectResponse = globals()[projectName]("", nodeID, nodeInt) # Call the project handler under test
projectResponse = globals()[projectName]("", nodeID, deviceID) # call the handler function once to start
while True: # represents the onReceive() loop in the bot.py
projectResponse = ""
responseLength = 0
@@ -51,7 +73,7 @@ if __name__ == '__main__': # represents the bot's main loop
packet = input(f"CLIENT {nodeID} INPUT: " ) # Emulate the client input
if packet != "":
#try:
projectResponse = globals()[projectName](message = packet, nodeID = nodeID, deviceID = nodeInt)
projectResponse = globals()[projectName](message = packet, nodeID = nodeID, deviceID = deviceID) # call the handler function
# except Exception as e:
# logger.error(f"System: Handler: {e}")
# projectResponse = "Error in handler"

View File

@@ -1,159 +1,378 @@
#!/bin/bash
# meshing-around install helper script
# install.sh
cd "$(dirname "$0")"
program_path=$(pwd)
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
printf "\n########################"
printf "\nMeshing Around Installer\n"
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
printf "\nChecking for dependencies\n"
printf "########################\n"
printf "\nThis script will try and install the Meshing Around Bot and its dependencies.\n"
printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded systems.\n"
printf "If there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# check if we are in /opt/meshing-around
if [ $program_path != "/opt/meshing-around" ]; then
printf "\nIt is suggested to project path to /opt/meshing-around\n"
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
read move
if [[ $(echo "$move" | grep -i "^y") ]]; then
sudo mv $program_path /opt/meshing-around
cd /opt/meshing-around
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
exit 0
fi
fi
# check write access to program path
if [[ ! -w ${program_path} ]]; then
printf "\nInstall path not writable, try running the installer with sudo\n"
exit 1
fi
# if hostname = femtofox, then we are on embedded
if [[ $(hostname) == "femtofox" ]]; then
printf "\nDetected femtofox embedded system\n"
embedded="y"
else
# check if running on embedded
printf "\nAre You installing into an embedded system like a luckfox or -native? most should say no here (y/n)"
read embedded
fi
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
printf "\nDetected embedded skipping dependency installation\n"
else
# Check and install dependencies
if ! command -v python3 &> /dev/null
then
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
sudo apt-get install python3 python3-pip
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, trying 'apt-get install python3-pip'\n"
sudo apt-get install python3-pip
fi
# double check for python3 and pip
if ! command -v python3 &> /dev/null
then
printf "python3 not found, please install python3 with your OS\n"
exit 1
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
exit 1
fi
printf "\nDependencies installed\n"
fi
# add user to groups for serial access
printf "\nAdding user to dialout and tty groups for serial access\n"
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
sudo usermod -a -G bluetooth $USER
# check for pip
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
sudo apt-get install python3-pip
else
printf "python pip found\n"
fi
# copy service files
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
# generate config file, check if it exists
if [ -f config.ini ]; then
if [[ -f config.ini ]]; then
printf "\nConfig file already exists, moving to backup config.old\n"
mv config.ini config.old
fi
cp config.template config.ini
printf "\nConfig file generated\n"
printf "\nConfig files generated!\n"
# update lat,long in config.ini
latlong=$(curl --silent --max-time 20 https://ipinfo.io/loc || echo "48.50,-123.0")
IFS=',' read -r lat lon <<< "$latlong"
sed -i "s|lat = 48.50|lat = $lat|g" config.ini
sed -i "s|lon = -123.0|lon = $lon|g" config.ini
echo "lat,long updated in config.ini to $latlong"
# set virtual environment and install dependencies
printf "\nMeshing Around Installer\n"
echo "Do you want to install the bot in a virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
exit 1
else
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
#check if python3 has venv module
if [ -f venv/bin/activate ]; then
printf "\nFpund virtual environment for python\n"
else
sudo apt-get install python3-venv
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
exit 1
fi
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies
pip install -U -r requirements.txt
fi
# check if running on embedded
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
printf "\nDetected embedded skipping venv\n"
else
printf "\nSkipping virtual environment...\n"
# install dependencies
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [ $rpi == "y" ]; then
pip install -U -r requirements.txt --break-system-packages
printf "\nRecomended install is in a python virtual environment, do you want to use venv? (y/n)"
read venv
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3/venv error, please install python3-venv with your OS\n"
exit 1
else
echo "The Following could be messy, or take some time on slower devices."
echo "Creating virtual environment..."
#check if python3 has venv module
if [[ -f venv/bin/activate ]]; then
printf "\nFound virtual environment for python\n"
python3 -m venv venv
source venv/bin/activate
else
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
sudo apt-get install python3-venv
fi
# create virtual environment
python3 -m venv venv
# double check for python3-venv
if [[ -f venv/bin/activate ]]; then
printf "\nFound virtual environment for python\n"
source venv/bin/activate
else
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
exit 1
fi
printf "\nVirtual environment created\n"
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies to venv
pip install -U -r requirements.txt
fi
else
pip install -U -r requirements.txt
printf "\nSkipping virtual environment...\n"
# install dependencies to system
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [[ $(echo "${rpi}" | grep -i "^y") ]]; then
pip install -U -r requirements.txt --break-system-packages
else
pip install -U -r requirements.txt
fi
fi
fi
printf "\n\n"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
read bot
# if $1 is passed
if [[ $1 == "pong" ]]; then
bot="pong"
elif [[ $1 == "mesh" ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
bot="mesh"
else
printf "\n\n"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
echo "Pong bot is a simple bot for network testing"
echo "Mesh bot is a more complex bot more suited for meshing around"
echo "None will skip the service install"
read bot
fi
# set the correct path in the service file
replace="s|/dir/|$program_path/|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
# set the correct user in the service file?
whoami=$(whoami)
#ask if we should add a user for the bot
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
printf "\nDo you want to add a local user (meshbot) no login, for the bot? (y/n)"
read meshbotservice
fi
if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
sudo useradd -M meshbot
sudo usermod -L meshbot
sudo groupadd meshbot
sudo usermod -a -G meshbot meshbot
whoami="meshbot"
echo "Added user meshbot with no home directory"
else
whoami=$(whoami)
fi
# set basic permissions for the bot user
sudo usermod -a -G dialout $whoami
sudo usermod -a -G tty $whoami
sudo usermod -a -G bluetooth $whoami
echo "Added user $whoami to dialout, tty, and bluetooth groups"
sudo chown -R $whoami:$whoami $program_path/logs
sudo chown -R $whoami:$whoami $program_path/data
echo "Permissions set for meshbot on logs and data directories"
# set the correct user in the service file
replace="s|User=pi|User=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sudo systemctl daemon-reload
sed -i $replace etc/mesh_bot_w3.service
printf "\n service files updated\n"
# ask if emoji font should be installed for linux
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [ $emoji == "y" ]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
if [ $bot == "pong" ]; then
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
# install service for pong bot
sudo cp etc/pong_bot.service /etc/systemd/system/
sudo systemctl enable pong_bot.service
sudo systemctl daemon-reload
echo "to start pong bot service: systemctl start pong_bot"
service="pong_bot"
fi
if [ $bot == "mesh" ]; then
if [[ $(echo "${bot}" | grep -i "^m") ]]; then
# install service for mesh bot
sudo cp etc/mesh_bot.service /etc/systemd/system/
sudo systemctl enable mesh_bot.service
sudo systemctl daemon-reload
echo "to start mesh bot service: systemctl start mesh_bot"
service="mesh_bot"
fi
if [ $bot == "n" ]; then
if [ -f launch.sh ]; then
printf "\nTo run the bot, use the command: ./launch.sh\n"
./launch.sh
# check if running on embedded for final steps
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
# ask if emoji font should be installed for linux
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [[ $(echo "${emoji}" | grep -i "^y") ]]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
fi
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
printf "ollama pull gemma3:270m\n"
# ask if the user wants to install the LLM Ollama components
printf "\nDo you want to install the LLM Ollama components? (y/n)"
read ollama
if [[ $(echo "${ollama}" | grep -i "^y") ]]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if the user wants to install the LLM Ollama components
echo "Do you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [ $gemma == "y" ]; then
ollama pull gemma2:2b
# ask if want to install gemma3:latest
printf "\n Ollama install done now we can install the gemma3:270m components\n"
echo "Do you want to install the gemma3:270m components? (y/n)"
read gemma
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
ollama pull gemma3:270m
fi
fi
# ask if the user wants to edit the ollama service for API access
if [[ -f /etc/systemd/system/ollama.service ]]; then
printf "\nEdit /etc/systemd/system/ollama.service and add Environment=OLLAMA_HOST=0.0.0.0 for API? (y/n)"
read editollama
if [[ $(echo "${editollama}" | grep -i "^y") ]]; then
replace="s|\[Service\]|\[Service\]\nEnvironment=\"OLLAMA_HOST=0.0.0.0\"|g"
sudo sed -i "$replace" /etc/systemd/system/ollama.service
sudo systemctl daemon-reload
sudo systemctl restart ollama.service
printf "\nOllama service updated and restarted\n"
fi
# assume we want to enable ollama in config.ini
if [[ -f config.ini ]]; then
replace="s|ollama = False|ollama = True|g"
sed -i "$replace" config.ini
printf "\nOllama enabled in config.ini\n"
fi
fi
# document the service install
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo cp %s/etc/%s.service /etc/systemd/system/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
printf "To see logs and stop the service:\n" >> install_notes.txt
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
fi
read -p "Press enter to complete the installation, these commands saved to install_notes.txt"
printf "\nGood time to reboot? (y/n)"
read reboot
if [[ $(echo "${reboot}" | grep -i "^y") ]]; then
sudo reboot
fi
else
# we are on embedded
# replace "type = serial" with "type = tcp" in config.ini
replace="s|type = serial|type = tcp|g"
sed -i "$replace" config.ini
# replace "# hostname = meshtastic.local" with "hostname = localhost" in config.ini
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
sed -i "$replace" config.ini
printf "\nConfig file updated for embedded\n"
# add service dependency for meshtasticd into service file
#replace="s|After=network.target|After=network.target meshtasticd.service|g"
# Set up the meshing around service
sudo cp /opt/meshing-around/etc/$service.service /etc/systemd/system/$service.service
sudo systemctl daemon-reload
sudo systemctl enable $service.service
sudo systemctl start $service.service
# check if the cron job already exists
if ! crontab -l | grep -q "$chronjob"; then
# add the cron job to run the report_generator5.py script
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
printf "\nAdded cron job to run report_generator5.py\n"
else
printf "\nCron job already exists, skipping\n"
fi
printf "Reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
printf "To see logs and stop the service:\n" >> install_notes.txt
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
fi
echo "Good time to reboot? (y/n)"
read reboot
if [ $reboot == "y" ]; then
sudo reboot
fi
printf "\nInstallation complete!\n"
exit 0
# to uninstall the product run the following commands as needed
# sudo systemctl stop mesh_bot
# sudo systemctl disable mesh_bot
# sudo systemctl stop pong_bot
# sudo systemctl disable pong_bot
# sudo systemctl stop mesh_bot_reporting
# sudo systemctl disable mesh_bot_reporting
# sudo rm /etc/systemd/system/mesh_bot.service
# sudo rm /etc/systemd/system/mesh_bot_w3.service
# sudo rm /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl reset-failed
# sudo gpasswd -d meshbot dialout
# sudo gpasswd -d meshbot tty
# sudo gpasswd -d meshbot bluetooth
# sudo groupdel meshbot
# sudo userdel meshbot
# sudo rm -rf /opt/meshing-around
# after install shenannigans
# add 'bee = True' to config.ini General section.
# wget https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a -O bee.txt

View File

@@ -1,4 +1,5 @@
#!/bin/bash
# This script launches the meshing-around bot or the report generator in python virtual environment
# launch.sh
cd "$(dirname "$0")"
@@ -25,8 +26,10 @@ elif [ "$1" == "html" ]; then
python3 etc/report_generator.py
elif [ "$1" == "html5" ]; then
python3 etc/report_generator5.py
elif [[ "$1" == add* ]]; then
python3 script/addFav.py
else
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5)"
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5) or addFav"
exit 1
fi

View File

@@ -1,26 +1,41 @@
# Logs and Reports
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed.
- `multi_log_reader = True` on by default will read all logs (or set to false to return daily logs)
## Reporting Note
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
- provided serviceTimer templates in etc/
- If you are in a venv and using launch.sh you can `launch.sh html5`
![reportView](../etc/reporting.jpg)
## Settings
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
```
```conf
[general]
# logging to file of the non Bot messages only
LogMessagesToFile = False
# Logging of system messages to file, needed for reporting engine
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
## Web Reporting WebServer
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
find it at. http://localhost:8420
If you have linux-native running and errors such as..
```bash
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
socketserver.TCPServer.server_bind(self)
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
self.socket.bind(self.server_address)
```
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
```python
# Set the desired IP address
server_ip = '127.0.0.1'
```
# Set level for stdout handler
stdout_handler.setLevel(logging.INFO)
```

File diff suppressed because it is too large Load Diff

54
modules/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Modules and Adding stuff
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
## By following these steps, you can add a new bbs option to the bot.
1. **Define the Command Handler**:
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
```python
def handle_newcommand(message, message_from_id, deviceID):
return "This is a response from the new command."
```
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
2. **Add the Command to the Auto Response**:
Update the auto_response function in mesh_bot.py to include the new command:
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
#...
```
3. **Update the Trap List and Help**:
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
```python
#...
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
help_message = "Bot CMD?:newcommand, "
#...
```
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
```python
# newcommand Configuration
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
if newcommand_enabled:
trap_list_newcommand = ("newcommand",)
trap_list = trap_list + trap_list_newcommand
help_message = help_message + ", newcommand"
```
5. **Test the New Command**:
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
### Running a Shell command
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"switchON": lambda: call_external_script(message)
```
This would call the default script located in script/runShell.sh and return its output.

View File

@@ -3,6 +3,13 @@
import pickle # pip install pickle
from modules.log import *
import time
useSynchCompression = False
if useSynchCompression:
import zlib
from modules.system import send_raw_bytes
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
@@ -10,24 +17,43 @@ trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsin
bbs_messages = []
bbs_dm = []
def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
try:
with open('data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
new_bbs_messages = pickle.load(f)
if isinstance(new_bbs_messages, list):
for msg in new_bbs_messages:
#example [1, 'Welcome to meshBBS', 'Welcome to the BBS, please post a message!', 0]
msgHash = hash(tuple(msg[1:3])) # Create a hash of the message content (subject and body)
# Check if the message already exists in bbs_messages
if all(hash(tuple(existing_msg[1:3])) != msgHash for existing_msg in bbs_messages):
# if the message is not a duplicate, add it to bbs_messages Maintain the message ID sequence
new_id = len(bbs_messages) + 1
bbs_messages.append([new_id, msg[1], msg[2], msg[3]])
except FileNotFoundError:
logger.debug("System: bbsdb.pkl not found, creating new one")
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
try:
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
except Exception as e:
logger.error(f"System: Error creating bbsdb.pkl: {e}")
except Exception as e:
logger.error(f"System: Error loading bbsdb.pkl: {e}")
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
logger.debug("System: Creating new data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
def save_bbsdb():
global bbs_messages
# save the bbs messages to the database file
logger.debug("System: Saving data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
try:
logger.debug("System: Saving data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
except Exception as e:
logger.error(f"System: Error saving bbsdb: {e}")
def bbs_help():
# help message
@@ -39,7 +65,7 @@ def bbs_list_messages():
message_list = ""
for message in bbs_messages:
# message[0] is the messageID, message[1] is the subject
message_list += "Msg #" + str(message[0]) + " " + message[1] + "\n"
message_list += "[#" + str(message[0]) + "] " + message[1] + "\n"
# last newline removed
message_list = message_list[:-1]
@@ -69,7 +95,11 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
else:
return "Please specify a message number to delete."
def bbs_post_message(subject, message, fromNode):
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
# post a message to the bbsdb
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
thread = threadID
replyto = replytoID
# post a message to the bbsdb and assign a messageID
messageID = len(bbs_messages) + 1
@@ -77,15 +107,17 @@ 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 message length isnt three times the MESSAGE_CHUNK_SIZE
if len(message) > (3 * MESSAGE_CHUNK_SIZE):
return "Message too long, max length is " + str(3 * MESSAGE_CHUNK_SIZE) + " characters."
# 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)
# validate its not overlength by keeping in chunker limit
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
bbs_messages.append([messageID, subject, message, fromNode, now, thread, replyto])
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
# save the bbsdb
@@ -98,8 +130,10 @@ def bbs_read_message(messageID = 0):
if (messageID - 1) >= len(bbs_messages):
return "Message not found."
if messageID > 0:
fromNode = bbs_messages[messageID - 1][3]
fromNodeHex = hex(fromNode)[-4:]
message = bbs_messages[messageID - 1]
return f"Msg #{message[0]}\nMsg Body: {message[2]}"
return f"Msg #{message[0]}\nFrom:{fromNodeHex}\n{message[2]}"
else:
return "Please specify a message number to read."
@@ -115,7 +149,11 @@ def load_bbsdm():
# load the bbs messages from the database file
try:
with open('data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
new_bbs_dm = pickle.load(f)
if isinstance(new_bbs_dm, list):
for msg in new_bbs_dm:
if msg not in bbs_dm:
bbs_dm.append(msg)
except:
bbs_dm = [[1234567890, "Message", 1234567890]]
logger.debug("System: Creating new data/bbsdm.pkl")
@@ -128,6 +166,14 @@ def bbs_post_dm(toNode, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
return "DM Posted for node " + str(toNode)
# validate message length isnt three times the MESSAGE_CHUNK_SIZE
if len(message) > (3 * MESSAGE_CHUNK_SIZE):
return "Message too long, max length is " + str(3 * MESSAGE_CHUNK_SIZE) + " characters."
# validate not a duplicate message
for msg in bbs_dm:
if msg[0] == int(toNode) and msg[1].strip().lower() == message.strip().lower():
return "DM Posted for node " + str(toNode)
# append the message to the list
bbs_dm.append([int(toNode), message, int(fromNode)])
@@ -162,25 +208,83 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
def compress_data(data_to_compress):
# Prepare message as bytes
compressed = zlib.compress(data_to_compress.encode('utf-8'))
return compressed
def decompress_data(data_bytes):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
return msg
except Exception as e:
logger.warning(f"Error decompressing data: {e}")
return False
def bbs_receive_compressed(data_bytes, fromNode, RxNode):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
bbs_sync_posts(msg, fromNode, RxNode)
return msg
except Exception as e:
logger.error(f"Error decompressing BBS message: {e}")
return None
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# check if the bbs link is enabled
if bbs_link_whitelist != ['']:
if str(peerNode) not in bbs_link_whitelist:
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
return "System: BBS Link is disabled for your node."
if bbs_link_enabled == False:
return "System: BBS Link is disabled."
# respond when another bot asks for the bbs posts to sync
if "bbslink" in input.lower():
if "$" in input and "#" in input:
#store the message
subject = input.split("$")[1].split("#")[0]
body = input.split("#")[1]
bbs_post_message(subject, body, peerNode)
fromNodeHex = input.split("@")[1]
try:
bbs_post_message(subject, body, int(fromNodeHex, 16))
except:
logger.error(f"System: Error parsing bbslink from node {peerNode}: {input}")
fromNodeHex = hex(peerNode)
messageID = input.split(" ")[1]
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
if len(input.split(" ")) > 1:
try:
messageID = int(input.split(" ")[1]) + 1
except:
return "link error"
else:
return "link error"
# send message
# send message with delay to keep chutil happy
if messageID < len(bbs_messages):
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
logger.debug(f"System: wait to bbslink with peer " + str(peerNode))
fromNodeHex = hex(bbs_messages[messageID][3])
time.sleep(5 + responseDelay)
# every 5 messages add extra delay
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
msg = f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
if useSynchCompression:
compressed = compress_data(msg)
send_raw_bytes(peerNode, compressed)
logger.debug("System: Sent compressed bbslink message to peer " + str(peerNode))
else:
return msg
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))

179
modules/checklist.py Normal file
View File

@@ -0,0 +1,179 @@
# Checkin Checkout database module for the bot
# K7MHI Kelly Keeton 2024
import sqlite3
from modules.log import *
import time
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout")
def initialize_checklist_database():
# create the database
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
# Check if the checkin table exists, and create it if it doesn't
c.execute('''CREATE TABLE IF NOT EXISTS checkin
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
# Check if the checkout table exists, and create it if it doesn't
c.execute('''CREATE TABLE IF NOT EXISTS checkout
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
conn.commit()
conn.close()
logger.debug("System: Ensured data/checklist.db exists with required tables")
def checkin(name, date, time, location, notes):
location = ", ".join(map(str, location))
# checkin a user
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
# # remove any checkouts that are older than the checkin
# c.execute("DELETE FROM checkout WHERE checkout_date < ? OR (checkout_date = ? AND checkout_time < ?)", (date, date, time))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_checklist_database()
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
else:
raise
conn.commit()
conn.close()
if reverse_in_out:
return "Checked✅Out: " + str(name)
else:
return "Checked✅In: " + str(name)
def delete_checkin(checkin_id):
# delete a checkin
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
c.execute("DELETE FROM checkin WHERE checkin_id = ?", (checkin_id,))
conn.commit()
conn.close()
return "Checkin deleted." + str(checkin_id)
def checkout(name, date, time_str, location, notes):
location = ", ".join(map(str, location))
# checkout a user
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
# Check if the user has a checkin before checking out
c.execute("""
SELECT checkin_id FROM checkin
WHERE checkin_name = ?
AND NOT EXISTS (
SELECT 1 FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
ORDER BY checkin_date DESC, checkin_time DESC
LIMIT 1
""", (name,))
checkin_record = c.fetchone()
if checkin_record:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
# calculate length of time checked in
c.execute("SELECT checkin_time FROM checkin WHERE checkin_id = ?", (checkin_record[0],))
checkin_time = c.fetchone()[0]
checkin_datetime = time.strptime(date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds))
# # remove the checkin record older than the checkout
# c.execute("DELETE FROM checkin WHERE checkin_date < ? OR (checkin_date = ? AND checkin_time < ?)", (date, date, time_str))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_checklist_database()
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
else:
raise
conn.commit()
conn.close()
if checkin_record:
if reverse_in_out:
return "Checked⌛In: " + str(name) + " duration " + timeCheckedIn
else:
return "Checked⌛Out: " + str(name) + " duration " + timeCheckedIn
else:
return "None found for " + str(name)
def delete_checkout(checkout_id):
# delete a checkout
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
c.execute("DELETE FROM checkout WHERE checkout_id = ?", (checkout_id,))
conn.commit()
conn.close()
return "Checkout deleted." + str(checkout_id)
def list_checkin():
# list checkins
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
c.execute("""
SELECT * FROM checkin
WHERE checkin_id NOT IN (
SELECT checkin_id FROM checkout
WHERE checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time)
)
""")
rows = c.fetchall()
conn.close()
timeCheckedIn = ""
checkin_list = ""
for row in rows:
# Calculate length of time checked in, including days
total_seconds = time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))
days = int(total_seconds // 86400)
hours = int((total_seconds % 86400) // 3600)
minutes = int((total_seconds % 3600) // 60)
seconds = int(total_seconds % 60)
if days > 0:
timeCheckedIn = f"{days}d {hours:02}:{minutes:02}:{seconds:02}"
else:
timeCheckedIn = f"{hours:02}:{minutes:02}:{seconds:02}"
checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn
if row[5] != "":
checkin_list += "📝" + row[5]
if row != rows[-1]:
checkin_list += "\n"
# if empty list
if checkin_list == "":
return "No data to display."
return checkin_list
def process_checklist_command(nodeID, message, name="none", location="none"):
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
# if user on bbs_ban_list reject command
if str(nodeID) in bbs_ban_list:
logger.warning("System: Checklist attempt from the ban list")
return "unable to process command"
try:
comment = message.split(" ", 1)[1]
except IndexError:
comment = ""
# handle checklist commands
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
return checkin(name, current_date, current_time, location, comment)
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
return checkout(name, current_date, current_time, location, comment)
elif "purgein" in message.lower():
return delete_checkin(nodeID)
elif "purgeout" in message.lower():
return delete_checkout(nodeID)
elif "?" in message.lower():
if not reverse_in_out:
return ("Command: checklist followed by\n"
"checkout to check out\n"
"purgeout to delete your checkout record\n"
"Example: checkin Arrived at park")
else:
return ("Command: checklist followed by\n"
"checkin to check out\n"
"purgeout to delete your checkin record\n"
"Example: checkout Leaving park")
elif "checklist" in message.lower():
return list_checkin()
else:
return "Invalid command."

View File

@@ -3,18 +3,56 @@
from modules.log import *
import asyncio
import random
import os
import subprocess
async def watch_file():
def read_file(file_monitor_file_path):
try:
with open(file_monitor_file_path, 'r') as f:
trap_list_filemon = ("readnews",)
NEWS_DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
newsSourcesList = []
def read_file(file_monitor_file_path, random_line_only=False):
try:
if not os.path.exists(file_monitor_file_path):
if file_monitor_file_path == "bee.txt":
return "🐝buzz 💐buzz buzz🍯"
if random_line_only:
# read a random line from the file
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
return random.choice(lines)
else:
# read the whole file
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
content = f.read()
return content
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
def read_news(source=None):
# Reads the news file. If a source is provided, reads {source}_news.txt.
if source:
file_path = os.path.join(NEWS_DATA_DIR, f"{source}_news.txt")
else:
file_path = os.path.join(NEWS_DATA_DIR, news_file_path)
return read_file(file_path, news_random_line_only)
def write_news(content, append=False):
# write the news file on demand
try:
file_path = os.path.join(NEWS_DATA_DIR, news_file_path)
with open(file_path, 'a' if append else 'w', encoding='utf-8') as f:
#f.write(content)
logger.info(f"FileMon: Updated {file_path}")
return True
except Exception as e:
logger.warning(f"FileMon: Error writing file: {file_path}")
return False
async def watch_file():
# Watch the file for changes and return the new content when it changes
if not os.path.exists(file_monitor_file_path):
return None
else:
@@ -29,4 +67,111 @@ async def watch_file():
content = content.replace('\n', ' ').replace('\r', '').strip()
if content:
return content
await asyncio.sleep(1) # Check every
await asyncio.sleep(1) # Check every
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
try:
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
if not os.path.exists(script_path):
# try the raw script name
script_path = script
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
# Use subprocess.run for better resource management
result = subprocess.run(
["bash", script_path, message],
capture_output=True,
text=True,
timeout=10
)
output = result.stdout.strip()
return output
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
return None
waitingXroom = {} # {message_from_id: (expected_answer, original_command, timestamp)}
def handleShellCmd(message, message_from_id, channel_number, isDM, deviceID):
if not allowXcmd:
return "x: command is disabled"
if str(message_from_id) not in bbs_admin_list:
logger.warning(f"FileMon: Unauthorized x: command attempt from {message_from_id}")
return "x: command not authorized"
if not isDM:
return "x: command not authorized in group chat"
# 2FA logic
if xCmd2factorEnabled:
timeNOW = datetime.utcnow()
# If user is waiting for 2FA, treat message as answer
if message_from_id in waitingXroom:
answer = message[2:].strip() if message.lower().startswith("x:") else message.strip()
expected, orig_command, ts = waitingXroom[message_from_id]
if timeNOW - ts > timedelta(seconds=xCmd2factor_timeout):
del waitingXroom[message_from_id]
return "x2FA timed out, please try again"
if answer == str(expected):
del waitingXroom[message_from_id]
# Run the original command
try:
logger.info(f"FileMon: Running shell command from {message_from_id}: {orig_command}")
result = subprocess.run(orig_command, shell=True, capture_output=True, text=True, timeout=10, start_new_session=True)
output = result.stdout.strip()
return output if output else "✅ x: processed finished, no output"
except Exception as e:
logger.warning(f"FileMon: Error running shell command: {e}")
logger.debug(f"FileMon: This command is not good for use over the mesh network")
return "x: error running command"
else:
logger.warning(f"FileMon: 🚨Incorrect 2FA answer from {message_from_id}")
return "x2FA incorrect, try again"
# If not waiting, treat as new command and issue challenge
if message.lower().startswith("x:"):
command = message[2:].strip()
# Generate two random numbers, seed with message_from_id and time of day
seed = timeNOW.second + timeNOW.minute * 60 + timeNOW.hour * 3600 + int(message_from_id)
rnd = random.Random(seed)
a = rnd.randint(10, 99)
b = rnd.randint(10, 99)
expected = a + b
waitingXroom[message_from_id] = (expected, command, timeNOW)
return f"x2FA required.\nReply `x: answer`\nWhat is {a} + {b}? "
else:
return "invalid command format"
# If we reach here, 2FA is disabled or passed
if enable_runShellCmd:
if message.lower().startswith("x:"):
command = message[2:].strip()
else:
return "invalid command format"
try:
logger.info(f"FileMon: Running shell command from {message_from_id}: {command}")
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10, start_new_session=True)
output = result.stdout.strip()
return output if output else "x: command executed with no output"
except Exception as e:
logger.warning(f"FileMon: Error running shell command: {e}")
logger.debug(f"FileMon: This command is not good for use over the mesh network")
return "error running command"
else:
logger.debug("FileMon: x: command is disabled by no enable_runShellCmd")
return "command is disabled"
def initNewsSources():
#check for the files _news.txt and add to the newsHeadlines list
global newsSourcesList
newsSourcesList = []
for file in os.listdir(NEWS_DATA_DIR):
if file.endswith('_news.txt'):
source = file[:-9] # remove _news.txt
newsSourcesList.append(source)
#initialize the headlines on startup
initNewsSources()

View File

@@ -7,8 +7,7 @@ import time
import pickle
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}]
from modules.settings import jackTracker
SUITS = ("♥️", "♦️", "♠️", "♣️")
RANKS = (
@@ -114,22 +113,35 @@ class jackChips:
self.total -= self.bet
self.winnings -= 1
def success_rate(card, obj_h):
""" Calculate Success rate of 'HIT' new cards """
msg = ""
rate = 0
diff = 21 - obj_h.value
if diff != 0:
rate = (VALUES[card[0][1]] / diff) * 100
def success_rate(next_card, player_hand):
# Estimate the chance of a successful 'HIT' (not busting) in blackjack.
if rate < 100:
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
else:
l_rate = int(rate - (rate - 99)) # Round to 99
if card[0][1] == "A":
l_rate -= 99
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
return msg
# If player already has 21 or more, hitting will always bust
if player_hand.value >= 21:
return "\n🧠 What do you think?"
# Calculate how much more the player can add without busting
max_safe = 21 - player_hand.value
safe_cards = 0
total_cards = 0
for rank in VALUES:
# 4 cards of each rank in a standard deck
count = 4
card_value = VALUES[rank]
# Ace can be 1 or 11, but here we treat it as 1 if 11 would bust
if rank == "A":
card_value = 1 if player_hand.value + 11 > 21 else 11
# Count as safe if it won't bust the player
if card_value <= max_safe:
safe_cards += count
total_cards += count
# Calculate probability
success_chance = int((safe_cards / total_cards) * 100)
fail_chance = 100 - success_chance
return f"\n🧠Hit: {fail_chance}% 👎, {success_chance}% 👍"
def hits(obj_de):
new_card = [obj_de.deal_cards()[0][0]]
@@ -147,12 +159,12 @@ def display_hand(hand):
def show_some(player_cards, dealer_cards, obj_h):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
msg += f"\nDealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
return msg
def show_all(player_cards, dealer_cards, obj_h, obj_d):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
msg += f"\nDealer[{obj_d.value}] {display_hand(dealer_cards)}"
return msg
def player_bust(obj_h, obj_c):
@@ -229,7 +241,7 @@ def loadHSJack():
pickle.dump(highScore, file)
return 0
def playBlackJack(nodeID, message):
def playBlackJack(nodeID, message, last_cmd=None):
# Initalize the Game
msg, last_cmd = '', None
blackJack = False
@@ -267,10 +279,12 @@ def playBlackJack(nodeID, message):
if last_cmd is None:
# create new player if not in tracker
logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"Welcome to ♠BlackJack♣ you have {p_chips.total} chips. Whats your bet?"
if nodeID != 0:
#logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"You have {p_chips.total} chips. Whats your bet?"
return "Error: Player not found."
if getLastCmdJack(nodeID) == "new":
# Place Bet
@@ -283,24 +297,26 @@ def playBlackJack(nodeID, message):
#resend the hand
msg += show_some(p_cards, d_cards, p_hand)
return msg
elif "blackjack" in message.lower():
return f"\nTo place a bet, enter the amount you wish to wager."
else:
try:
bet_money = int(message)
except ValueError:
return "Invalid Bet, please enter a valid number."
return f"\nInvalid Bet, please enter a valid number."
if bet_money <= p_chips.total and bet_money >= 1:
p_chips.bet = bet_money
else:
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
return f"\nInvalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
except ValueError:
return f"Invalid Bet, the maximum bet, {p_chips.total}"
return f"\nInvalid Bet, the maximum bet, {p_chips.total}"
# Show the cards
msg += show_some(p_cards, d_cards, p_hand)
# check for blackjack 21 and only two cards
if p_hand.value == 21 and len(p_hand.cards) == 2:
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
msg += f"\n🎰 BLAAAACKJACKKKK 💰"
p_chips.total += round(p_chips.bet * 1.5)
setLastCmdJack(nodeID, "dealerTurn")
blackJack = True
@@ -317,7 +333,7 @@ def playBlackJack(nodeID, message):
if getLastCmdJack(nodeID) == "betPlaced":
setLastCmdJack(nodeID, "playing")
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
msg += f"\n(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
# save the game state
for i in range(len(jackTracker)):
@@ -367,7 +383,7 @@ def playBlackJack(nodeID, message):
# Check if player bust
if player_bust(p_hand, p_chips):
d_win += 1
msg += "💥PlayerBUST💥"
msg += f"\n💥PlayerBUST💥"
setLastCmdJack(nodeID, "dealerTurn")
if getLastCmdJack(nodeID) == "playing":
@@ -419,7 +435,7 @@ def playBlackJack(nodeID, message):
d_hand.add_cards(d_card)
if dealer_bust(d_hand, p_hand, p_chips):
p_win += 1
msg += "💰DealerBUST💥"
msg += f"\n💰DealerBUST💥"
break
# Show all cards
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
@@ -427,15 +443,15 @@ def playBlackJack(nodeID, message):
# Check who wins
if push(p_hand, d_hand):
draw += 1
msg += "👌PUSH"
msg += f"\n👌PUSH"
elif player_wins(p_hand, d_hand, p_chips):
p_win += 1
msg += "🎉PLAYER WINS🎰"
msg += f"\n🎉PLAYER WINS🎰"
elif dealer_wins(p_hand, d_hand, p_chips):
d_win += 1
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
else:
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
# Display the Game Stats
msg += gameStats(str(p_win), str(d_win), str(draw))
@@ -443,20 +459,20 @@ def playBlackJack(nodeID, message):
# Display the chips left
if p_chips.total < 1:
if p_chips.total > 0:
msg += "🪙Keep the change you filthy animal!"
msg += f"\n🪙Keep the change you filthy animal!"
else:
msg += "💸NO MORE CHIPS!🏧💳"
msg += f"\n💸NO MORE CHIPS!🏧💳"
p_chips.total = jack_starting_cash
else:
# check high score
highScore = loadHSJack()
if highScore != 0 and p_chips.total > highScore['highScore']:
msg += f"💰HighScore💰{p_chips.total} "
msg += f"\n💰HighScore💰{p_chips.total} "
saveHSJack(nodeID, p_chips.total)
else:
msg += f"💰You have {p_chips.total} chips "
msg += f"\n💰You have {p_chips.total} chips "
msg += " Bet or Leave?"
msg += f"\nBet or Leave?"
# Reset the game
setLastCmdJack(nodeID, "new")
@@ -468,6 +484,6 @@ def playBlackJack(nodeID, message):
jackTracker[i]['d_cards'] = []
jackTracker[i]['p_hand'] = []
jackTracker[i]['d_hand'] = []
jackTracker[i]['time'] = time.time()
jackTracker[i]['last_played'] = time.time()
return msg

View File

@@ -14,7 +14,7 @@ dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
from modules.settings import dwPlayerTracker
# high score is saved in a pickle file
dwHighScore = {}
@@ -159,7 +159,7 @@ def get_found_items(nodeID):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
dwInventoryDb[i]['amount'] = amount
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
msg = f"💊You found {qty} {my_drugs[found].name}"
else:
# rolls to see how much cash the user finds
cash_found = random.randint(1, 977)
@@ -241,6 +241,8 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
buy_amount = cash // price_list[drug_choice]
if buy_amount > 100 - inventory:
buy_amount = 100 - inventory
if buy_amount == 0:
return "You don\'t have any empty inventory slots.🎒"
# set the buy amount to the max if the user enters m
buy_amount = int(buy_amount)
@@ -364,7 +366,8 @@ def get_location_table(nodeID, choice=0):
return loc_table_string
def endGameDw(nodeID):
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore, dwPlayerTracker
cash = 0
msg = ''
dwHighScore = getHighScoreDw()
# Confirm the cash for the user
@@ -373,23 +376,6 @@ def endGameDw(nodeID):
cash = dwCashDb[i].get('cash')
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
# remove the player from the game databases
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb.pop(i)
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb.pop(i)
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
dwLocationDb.pop(i)
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
dwGameDayDb.pop(i)
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker.pop(i)
# checks if the player's score is higher than the high score and writes a new high score if it is
if cash > dwHighScore.get('cash'):
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
@@ -678,6 +664,7 @@ def playDopeWars(nodeID, cmd):
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
dwPlayerTracker[i]['last_played'] = time.time()
# Game end
if game_day == total_days + 1:

View File

@@ -26,7 +26,7 @@ par4_5_range = par4_range + par5_range
# Player setup
playingHole = False
golfTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'hole': 0, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''}]
from modules.settings import golfTracker
# Club functions
def hit_driver():
@@ -122,9 +122,8 @@ def getHighScoreGolf(nodeID, strokes, par):
return 0
# Main game loop
def playGolf(nodeID, message, finishedHole=False):
def playGolf(nodeID, message, finishedHole=False, last_cmd=''):
msg = ''
global golfTracker
# Course setup
par3_count = 0
par4_count = 0
@@ -133,6 +132,7 @@ def playGolf(nodeID, message, finishedHole=False):
total_strokes = 0
total_to_par = 0
par = 0
hole = 1
# get player's last command from tracker if not new player
last_cmd = ""
@@ -145,8 +145,12 @@ def playGolf(nodeID, message, finishedHole=False):
par = golfTracker[i]['par']
total_strokes = golfTracker[i]['total_strokes']
total_to_par = golfTracker[i]['total_to_par']
if last_cmd == "" or last_cmd == "new":
#update last played time
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['last_played'] = time.time()
if last_cmd == "new":
# Start a new hole
if hole <= 9:
# Set up hole count restrictions on par
@@ -193,17 +197,19 @@ def playGolf(nodeID, message, finishedHole=False):
# Set initial parameters before starting a hole
distance_remaining = hole_length
hole_shots = 0
last_cmd = 'stroking'
# save player's current game state
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['cmd'] = last_cmd
golfTracker[i]['hole'] = hole
golfTracker[i]['distance_remaining'] = distance_remaining
golfTracker[i]['cmd'] = 'stroking'
golfTracker[i]['par'] = par
golfTracker[i]['total_strokes'] = total_strokes
golfTracker[i]['total_to_par'] = total_to_par
golfTracker[i]['hazard'] = hazard
golfTracker[i]['hole'] = hole
golfTracker[i]['last_played'] = time.time()
golfTracker[i]['hole_shots'] = hole_shots
@@ -320,8 +326,8 @@ def playGolf(nodeID, message, finishedHole=False):
else:
last_cmd = 'stroking'
else:
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
msg += "\nClub?[D, L, M, H, G, W]🏌️"
msg += f"\nYou have " + str(distance_remaining) + "yd. ⛳️"
msg += f"\nClub?[D, L, M, H, G, W]🏌️"
# save player's current game state, keep stroking
@@ -365,7 +371,7 @@ def playGolf(nodeID, message, finishedHole=False):
if hole not in [1, 10]:
# Show player total scoring info for the round, except hole 1 and 10
msg += "\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
msg += f"\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
msg += getScorecardGolf(total_to_par)
# Move to next hole
@@ -403,7 +409,7 @@ def playGolf(nodeID, message, finishedHole=False):
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
else:
# Show player the next hole
msg += playGolf(nodeID, 'new', True)
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
msg += playGolf(nodeID, '', True, last_cmd='new')
msg += f"\n🏌️[D, L, M, H, G, W, End]🏌️"
return msg

142
modules/games/hamtest.py Normal file
View File

@@ -0,0 +1,142 @@
# hamradio test module for meshbot DE K7MHI 2025
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
# data files which are expected to be in ../../data/hamradio/ similar to the following:
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
import json
import random
import os
from modules.log import *
class HamTest:
def __init__(self):
self.questions = {}
self.load_questions()
self.game = {}
def load_questions(self):
for level in ['technician', 'general', 'extra']:
try:
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
self.questions[level] = json.load(f)
except FileNotFoundError:
logger.error(f"File not found: ../../data/hamradio/{level}.json")
self.questions[level] = []
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
self.questions[level] = []
def newGame(self, id, level='technician'):
msg = f"📻New {level} quiz started, 'end' to exit."
if id in self.game:
level = self.game[id]['level']
self.game[id] = {
'level': level,
'score': 0,
'total': 0,
'errors': [],
'qId': None,
'question': None,
'answers': None,
'correct': None
}
# set the pool needed for the game
if self.game[id]['level'] == 'extra':
self.game[id]['total'] = 50
else:
self.game[id]['total'] = 35
# randomize the questions
random.shuffle(self.questions[level])
msg += f"\n{self.nextQuestion(id)}"
return msg
def nextQuestion(self, id):
level = self.game[id]['level']
# if question has the word figure in it, skip it
question = random.choice(self.questions[level])
while 'figure' in question['question'].lower():
question = random.choice(self.questions[level])
self.game[id]['question'] = question['question']
self.game[id]['answers'] = question['answers']
self.game[id]['correct'] = question['correct']
self.game[id]['qId'] = question['id']
self.game[id]['total'] -= 1
if self.game[id]['total'] == 0:
return self.endGame(id)
# ask the question and return answers in A, B, C, D format
msg = f"{self.game[id]['question']}\n"
for i, answer in enumerate(self.game[id]['answers']):
msg += f"{chr(65+i)}. {answer}\n"
return msg
def answer(self, id, answer):
if id not in self.game:
return "No game in progress"
if self.game[id]['correct'] == ord(answer.upper()) - 65:
self.game[id]['score'] += 1
return f"Correct👍\n" + self.nextQuestion(id)
else:
# record the section of the question for study aid
section = self.game[id]['qId'][:3]
self.game[id]['errors'].append(section)
# provide the correct answer
answer = [self.game[id]['correct']]
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
def getScore(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
total = self.game[id]['total']
level = self.game[id]['level']
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
if level == 'extra':
pool = 50
else:
pool = 35
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
def endGame(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
level = self.game[id]['level']
if level == 'extra':
# passing score for extra is 37 out of 50
passing = 37
else:
# passing score for technician and general is 26 out of 35
passing = 26
if score >= passing:
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
else:
# find the most common section of the questions missed
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
# remove the game[id] from the list
del self.game[id]
return msg
hamtestTracker = []
hamtest = HamTest()

203
modules/games/hangman.py Normal file
View File

@@ -0,0 +1,203 @@
# Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
import random
class Hangman:
WORDS = [
"ability","able","about","above","accept","according","account","across",
"act","action","activity","actually","add","address","administration","admit",
"adult","affect","after","again","against","age","agency","agent","ago",
"agree","agreement","ahead","air","all","allow","almost","alone","along",
"already","also","although","always","American","among","amount","analysis",
"and","animal","another","answer","any","anyone","anything","appear","apply",
"approach","area","argue","arm","around","arrive","art","article","artist",
"as","ask","assume","at","attack","attention","attorney","audience","author",
"authority","available","avoid","away","baby","back","bad","bag","ball",
"bank","bar","base","be","beat","beautiful","because","become","bed","before",
"begin","behavior","behind","believe","benefit","best","better","between",
"beyond","big","bill","billion","bit","black","blood","blue","board","body",
"book","born","both","box","boy","break","bring","brother","budget","build",
"building","business","but","buy","by","call","camera","campaign","can",
"cancer","candidate","capital","car","card","care","career","carry","case",
"catch","cause","cell","center","central","century","certain","certainly",
"chair","challenge","chance","change","character","charge","check","child",
"choice","choose","church","citizen","city","civil","claim","class","clear",
"clearly","close","coach","cold","collection","college","color","come",
"commercial","common","community","company","compare","computer","concern",
"condition","conference","Congress","consider","consumer","contain","continue",
"control","cost","could","country","couple","course","court","cover","create",
"crime","cultural","culture","cup","current","customer","cut","dark","data",
"daughter","day","dead","deal","death","debate","decade","decide","decision",
"deep","defense","degree","democrat","democratic","describe","design",
"despite","detail","determine","develop","development","die","difference",
"different","difficult","dinner","direction","director","discover","discuss",
"discussion","disease","do","doctor","dog","door","down","draw","dream","drive",
"drop","drug","during","each","early","east","easy","eat","economic","economy",
"edge","education","effect","effort","eight","either","election","else",
"employee","end","energy","enjoy","enough","enter","entire","environment",
"environmental","especially","establish","even","evening","event","ever",
"every","everybody","everyone","everything","evidence","exactly","example",
"executive","exist","expect","experience","expert","explain","eye","face",
"fact","factor","fail","fall","family","far","fast","father","fear","federal",
"feel","feeling","few","field","fight","figure","fill","film","final","finally",
"financial","find","fine","finger","finish","fire","firm","first","fish","five",
"floor","fly","focus","follow","food","foot","for","force","foreign","forget",
"form","former","forward","four","free","friend","from","front","full","fund",
"future","game","garden","gas","general","generation","get","girl","give",
"glass","go","goal","good","government","great","green","ground","group","grow",
"growth","guess","gun","guy","hair","half","hand","hang","happen","happy",
"hard","have","he","head","health","hear","heart","heat","heavy","help","her",
"here","herself","high","him","himself","his","history","hit","hold","home",
"hope","hospital","hot","hotel","hour","house","how","however","huge","human",
"hundred","husband","I","idea","identify","if","image","imagine","impact",
"important","improve","in","include","including","increase","indeed","indicate",
"individual","industry","information","inside","instead","institution","interest",
"interesting","international","interview","into","investment","involve","issue",
"it","item","its","itself","job","join","just","keep","key","kid","kill","kind",
"kitchen","know","knowledge","land","language","large","last","late","later",
"laugh","law","lawyer","lay","lead","leader","learn","least","leave","left",
"leg","legal","less","let","letter","level","lie","life","light","like","likely",
"line","list","listen","little","live","local","long","look","lose","loss",
"lot","love","low","machine","magazine","main","maintain","major","majority",
"make","man","manage","management","manager","many","market","marriage",
"material","matter","may","maybe","me","mean","measure","media","medical","meet",
"meeting","member","memory","mention","message","method","middle","might",
"military","million","mind","minute","miss","mission","model","modern","moment",
"money","month","more","morning","most","mother","mouth","move","movement",
"movie","Mr","Mrs","much","music","must","my","myself","name","nation",
"national","natural","nature","near","nearly","necessary","need","network",
"never","new","news","newspaper","next","nice","night","no","none","nor",
"north","not","note","nothing","notice","now","number","occur","of","off",
"offer","office","officer","official","often","oh","oil","ok","old","on",
"once","one","only","onto","open","operation","opportunity","option","or",
"order","organization","other","others","our","out","outside","over","own",
"owner","page","pain","painting","paper","parent","part","participant",
"particular","particularly","partner","party","pass","past","patient","pattern",
"pay","peace","people","per","perform","performance","perhaps","period",
"person","personal","phone","physical","pick","picture","piece","place","plan",
"plant","play","player","point","police","policy","political","politics",
"poor","popular","population","position","positive","possible","power",
"practice","prepare","present","president","pressure","pretty","prevent","price",
"private","probably","problem","process","produce","product","production",
"professional","professor","program","project","property","protect","prove",
"provide","public","pull","purpose","push","put","quality","question","quickly",
"quite","race","radio","raise","range","rate","rather","reach","read","ready",
"real","reality","realize","really","reason","receive","recent","recently",
"recognize","record","red","reduce","reflect","region","relate","relationship",
"religious","remain","remember","remove","report","represent","republican",
"require","research","resource","respond","response","responsibility","rest",
"result","return","reveal","rich","right","rise","risk","road","rock","role",
"room","rule","run","safe","same","save","say","scene","school","science",
"scientist","score","sea","season","seat","second","section","security","see",
"seek","seem","sell","send","senior","sense","series","serious","serve",
"service","set","seven","several","shake","share","she","shoot","short","shot",
"should","shoulder","show","side","sign","significant","similar","simple",
"simply","since","sing","single","sister","sit","site","situation","six","size",
"skill","skin","small","smile","so","social","society","soldier","some",
"somebody","someone","something","sometimes","son","song","soon","sort","sound",
"source","south","southern","space","speak","special","specific","speech",
"spend","sport","spring","staff","stage","stand","standard","star","start",
"state","statement","station","stay","step","still","stock","stop","store",
"story","strategy","street","strong","structure","student","study","stuff",
"style","subject","success","successful","such","suddenly","suffer","suggest",
"summer","support","sure","surface","system","table","take","talk","task","tax",
"teach","teacher","team","technology","television","tell","ten","tend","term",
"test","than","thank","that","the","their","them","themselves","then","theory",
"there","these","they","thing","think","third","this","those","though","thought",
"thousand","threat","three","through","throughout","throw","thus","time","to",
"today","together","tonight","too","top","total","tough","toward","town","trade",
"traditional","training","travel","treat","treatment","tree","trial","trip",
"trouble","true","truth","try","turn","TV","two","type","under","understand",
"unit","until","up","upon","us","use","usually","value","various","very",
"victim","view","violence","visit","voice","vote","wait","walk","wall","want",
"war","watch","water","way","we","weapon","wear","week","weight","well","west",
"western","what","whatever","when","where","whether","which","while","white",
"who","whole","whom","whose","why","wide","wife","will","win","wind","window",
"wish","with","within","without","woman","wonder","word","work","worker","world",
"worry","would","write","writer","wrong","yard","yeah","year","yes","yet","you",
"young","your","yourself","meshtastic","node","lora","mesh"]
def __init__(self):
self.game = {}
def new_game(self, id):
games = won = 0
ret = ""
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
ret += f"Total Games: {games}, Won: {won}\n"
self.game[id] = {
"word": self.random_word(),
"guesses": [],
"games": games+1,
"won": won
}
ret += self.game_continue(id)
return ret
def guess(self, id, input):
g = self.game[id]
if not input:
return
letter = input[0].lower()
if letter.isalpha() and letter not in g["guesses"]:
g["guesses"].append(letter)
def wrong_guesses(self, id):
g = self.game[id]
wrong = 0
for letter in g["guesses"]:
if letter not in g["word"]:
wrong += 1
return wrong
def won(self, id):
g = self.game[id]
for letter in g["word"]:
if letter not in g["guesses"]:
return False
return True
def mask(self, id):
g = self.game[id]
return " ".join([a if a in g["guesses"] else "_" for a in g["word"]])
def game_board(self, id):
g = self.game[id]
emotions = "😀🙂😐😑😕😔💀"
wrong = self.wrong_guesses(id)
ret = ""
if self.won(id):
ret += "🥳" + "\n"
g["won"] += 1
else:
ret += emotions[wrong] + "\n"
ret += hangman.mask(id) + "\n"
if g["guesses"]:
ret += ",".join(g["guesses"]) + "\n"
return ret
def game_continue(self, id):
return self.game_board(id) + "Guess a letter"
def game_over(self, id):
return self.game_board(id) + "Game over, the word was " + self.game[id]["word"]
def play(self, id, input):
if id not in self.game:
return self.new_game(id)
self.guess(id, input)
wrong = self.wrong_guesses(id)
if wrong >= 6 or self.won(id):
return self.game_over(id) + "\n" + self.new_game(id)
return self.game_continue(id)
def end(self, id):
del self.game[id]
def random_word(self):
return random.choice(self.WORDS)
hangmanTracker = []
hangman = Hangman()

View File

@@ -2,8 +2,61 @@
# The emoji table of contents is used to replace words in the joke with emojis
# As a Ham, is this obsecuring the meaning of the joke? Or is it enhancing it?
from dadjokes import Dadjoke # pip install dadjokes
import random
from modules.log import *
lameJokes = [
"Why don't scientists trust atoms? Because they make up everything!",
"Why did the scarecrow win an award? Because he was outstanding in his field!",
"Why don't skeletons fight each other? They don't have the guts.",
"What do you call fake spaghetti? An impasta!",
"Why did the bicycle fall over? Because it was two-tired!",
"Why did the math book look sad? Because it had too many problems.",
"Why did the golfer bring two pairs of pants? In case he got a hole in one.",
"Why did the coffee file a police report? It got mugged.",
"Why did the tomato turn red? Because it saw the salad dressing!",
"Why did the cookie go to the doctor? Because it felt crummy.",
"Why did the computer go to the doctor? Because it had a virus!",
"Why did the chicken join a band? Because it had the drumsticks!",
"Why did the banana go to the doctor? Because it wasn't peeling well.",
"Why did the cow go to space? To see the moooon!",
"Why did the fish blush? Because it saw the ocean's bottom!",
"Why did the elephant bring a suitcase to the zoo? Because it wanted to pack its trunk!",
"Why did the meshtastic node go to therapy? It had too many connections to handle!",
"Why did the meshtastic user bring a ladder to the meeting? To reach new heights in communication!",
"Why did the meshtastic device break up with Wi-Fi? It found a better connection!",
"Why did the meshtastic network throw a party? Because it wanted to mesh well with everyone!",
"Why did the meshtastic node get promoted? Because it was outstanding in its field!",
"Why did the meshtastic user bring a map? To navigate the mesh of possibilities!",
"Why did the meshtastic device go to school? To improve its signal strength!",
"How did the meshtastic node become a comedian? It uses mesh-bots to deliver punchlines!",
"Chuck Norris doesn't read books. He stares them down until he gets the information he wants.",
"When Chuck Norris enters a room, he doesn't turn the lights on. He turns the dark off.",
"Chuck Norris can divide by zero.",
"Chuck Norris counted to infinity. Twice.",
"Chuck Norris can slam a revolving door.",
"When Chuck Norris does a push-up, he isn't lifting himself up; he's pushing the Earth down.",
"Chuck Norris can hear sign language.",
"Death once had a near-Chuck Norris experience.",
"Chuck Norris can unscramble an egg.",
"Chuck Norris can win a game of Connect Four in only three moves.",
"Chuck Norris can make a snowman out of rain.",
"Chuck Norris can strangle you with a cordless phone.",
"Chuck Norris can do a wheelie on a unicycle.",
"Chuck Norris can kill two stones with one bird.",
"Chuck Norris can speak braille.",
"Chuck Norris can build a snowman out of rain.",
"Chuck Norris can hear sign language.",
"Death once had a near-Chuck Norris experience.",
"Chuck Norris can unscramble an egg.",
"Chuck Norris can win a game of Connect Four in only three moves.",
"Chuck Norris can make a snowman out of rain.",
"Chuck Norris can strangle you with a cordless phone.",
"Chuck Norris can do a wheelie on a unicycle.",
"Chuck Norris can kill two stones with one bird."]
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
def tableOfContents():
wordToEmojiMap = {
'love': '❤️', 'heart': '❤️', 'happy': '😊', 'smile': '😊', 'sad': '😢', 'angry': '😠', 'mad': '😠', 'cry': '😢', 'laugh': '😂', 'funny': '😂', 'cool': '😎',
@@ -115,8 +168,14 @@ def sendWithEmoji(message):
i += 1
return ' '.join(words)
def tell_joke(nodeID=0):
def tell_joke(nodeID=0, vox=False):
dadjoke = Dadjoke()
renderedLaugh = sendWithEmoji(dadjoke.joke)
return renderedLaugh
try:
if dad_jokes_emojiJokes or vox:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke
return renderedLaugh
except Exception as e:
return random.choice(lameJokes)

View File

@@ -18,12 +18,12 @@ locale.setlocale(locale.LC_ALL, '')
lemon_starting_cash = 30.00
lemon_total_weeks = 7
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'time': time.time()}]
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
from modules.settings import lemonadeTracker
def get_sales_amount(potential, unit, price):
"""Gets the sales amount.
@@ -50,12 +50,14 @@ def getHighScoreLemon():
pickle.dump(high_score, file)
return high_score
def start_lemonade(nodeID, message, celsius=False):
def playLemonstand(nodeID, message, celsius=False, newgame=False):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
msg = ""
potential = 0
unit = 0.0
price = 0.0
total_sales = 0
lemonsLastCmd = ''
high_score = getHighScoreLemon()
@@ -94,33 +96,6 @@ def start_lemonade(nodeID, message, celsius=False):
lemonadeScore[i]['value'] = score.value
lemonadeScore[i]['total'] = score.total
def endGame(nodeID):
# remove the player from the tracker
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker.pop(i)
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
lemonadeCups.pop(i)
for i in range(len(lemonadeLemons)):
if lemonadeLemons[i]['nodeID'] == nodeID:
lemonadeLemons.pop(i)
for i in range(len(lemonadeSugar)):
if lemonadeSugar[i]['nodeID'] == nodeID:
lemonadeSugar.pop(i)
for i in range(len(lemonadeWeeks)):
if lemonadeWeeks[i]['nodeID'] == nodeID:
lemonadeWeeks.pop(i)
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
lemonadeScore.pop(i)
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
# Check for end of game
if message.lower().startswith("e"):
endGame(nodeID)
return "Goodbye!👋"
title="LemonStand🍋"
# Define the temperature unit symbols
fahrenheit_unit = "ºF"
@@ -213,7 +188,7 @@ def start_lemonade(nodeID, message, celsius=False):
inventory.sugar = lemonadeTracker[i]['sugar']
inventory.cash = lemonadeTracker[i]['cash']
inventory.start = lemonadeTracker[i]['start']
last_cmd = lemonadeTracker[i]['cmd']
lemonsLastCmd = lemonadeTracker[i]['cmd']
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
cups.cost = lemonadeCups[i]['cost']
@@ -239,15 +214,35 @@ def start_lemonade(nodeID, message, celsius=False):
if lemonadeScore[i]['nodeID'] == nodeID:
score.value = lemonadeScore[i]['value']
score.total = lemonadeScore[i]['total']
if (newgame):
# reset the game values
inventory.cups = 0
inventory.lemons = 0
inventory.sugar = 0
inventory.cash = lemon_starting_cash
inventory.start = lemon_starting_cash
cups.cost = 2.50
cups.unit = round(cups.cost / cups.count, 2)
lemons.cost = 4.00
lemons.unit = round(lemons.cost / lemons.count, 2)
sugar.cost = 3.00
sugar.unit = round(sugar.cost / sugar.count, 2)
weeks.current = 1
weeks.total_sales = 0
weeks.summary = []
score.value = 0.00
score.total = 0.00
lemonsLastCmd = "cups"
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
lemonadeTracker[i]['last_played'] = time.time()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
# Start the main loop
if (weeks.current <= weeks.total):
if "new" in last_cmd:
# set the last command to cups in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
if newgame or "new" in lemonsLastCmd:
logger.debug("System: Lemonade: New Game: " + str(nodeID))
# Create a new display buffer for the text messages
buffer= ""
@@ -264,7 +259,7 @@ def start_lemonade(nodeID, message, celsius=False):
buffer += ". " + \
formatted + temperature.units + " " + \
forecastd[list(forecastd)[temperature.forecast]][2] + \
" " + glyph
" " + glyph + f"\n"
# Calculate the potential sales as a percentage of the maximum value
# (lower temperature = fewer sales, severe weather = fewer sales)
@@ -292,44 +287,39 @@ def start_lemonade(nodeID, message, celsius=False):
sugar.unit = round(sugar.cost / sugar.count, 2)
# Calculate the unit cost and display the estimated sales from the forecast potential
unit = cups.unit + lemons.unit + sugar.unit
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
buffer += " Sales Potential:" + str(potential) + " cups."
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
buffer += f"\nSupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
buffer += f"\nSales Potential:" + str(potential) + " cups."
# Display the current inventory
buffer += " Inventory:"
buffer += f"\nInventory:"
buffer += "🥤:" + str(inventory.cups)
buffer += "🍋:" + str(inventory.lemons)
buffer += "🍚:" + str(inventory.sugar)
# Display the updated item prices
buffer += f"\nPrices: "
buffer += "🥤:" + \
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + \
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + \
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
buffer += f"\nPrices:\n"
buffer += f"\n🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += f"\n🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += f"\n🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
# Display the current cash
gainloss = inventory.cash - inventory.start
buffer += " 💵:" + \
locale.currency(inventory.cash, grouping=True)
buffer += f"\n💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
# if the player is in the red
pnl = locale.currency(gainloss, grouping=True)
pnl = locale.currency(round(gainloss, 2), grouping=True)
if "0.00" not in pnl:
if pnl.startswith("-"):
buffer += "📊P&L📉" + pnl
else:
buffer += "📊P&L📈" + pnl
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
buffer += f"\n🥤 to buy?\nHave {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return buffer
if "cups" in last_cmd:
if "cups" in lemonsLastCmd and not newgame:
# Read the number of cup boxes to purchase
newcups = -1
if "n" in message.lower():
@@ -343,22 +333,22 @@ def start_lemonade(nodeID, message, celsius=False):
inventory.cups += (newcups * cups.count)
inventory.cash -= cost
msg = "Purchased " + str(newcups) + " 📦 "
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(round(inventory.cash, 2), grouping=True) + f" remaining"
else:
msg = "No 🥤 were purchased"
except Exception as e:
return "invalid input, enter the number of 🥤 to purchase or (N)one"
msg += f"\n 🍋 to buy?\nHave {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
# set the last command to lemons in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "lemons"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
return msg
if "lemons" in last_cmd:
if "lemons" in lemonsLastCmd and not newgame:
# Read the number of lemon bags to purchase
newlemons = -1
if "n" in message.lower():
@@ -379,15 +369,15 @@ def start_lemonade(nodeID, message, celsius=False):
newlemons = -1
return "invalid input, enter the number of 🍋 to purchase"
msg += f"\n 🍚 to buy?\nYou have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
# set the last command to sugar in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sugar"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
return msg
if "sugar" in last_cmd:
if "sugar" in lemonsLastCmd and not newgame:
# Read the number of sugar bags to purchase
newsugar = -1
if "n" in message.lower():
@@ -407,8 +397,8 @@ def start_lemonade(nodeID, message, celsius=False):
except Exception as e:
return "invalid input, enter the number of 🍚 bags to purchase"
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
msg += f"Cost of goods is {locale.currency(round(unit, 2), grouping=True)}"
msg += f"per 🥤 {locale.currency(round(inventory.cash, 2), grouping=True)} 💵 remaining."
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
# set the last command to price in the inventory db
@@ -418,17 +408,17 @@ def start_lemonade(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "price" in last_cmd:
if "price" in lemonsLastCmd and not newgame:
# set the last command to sales in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sales"
if "g" in message.lower():
lemonadeTracker[i]['cmd'] = "cups"
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
msg = f"#of🥤\nto buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
return msg
else:
last_cmd = "sales"
lemonsLastCmd = "sales"
# Read the actual price
price = 0.00
@@ -440,7 +430,7 @@ def start_lemonade(nodeID, message, celsius=False):
return "The price must be greater than zero."
except Exception as e:
price = 0.00
last_cmd = "price"
lemonsLastCmd = "price"
return "Invalid input, enter the price of the lemonade per 🥤"
# this isnt sent to the user, not needed
@@ -448,7 +438,7 @@ def start_lemonade(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
if "sales" in last_cmd:
if "sales" in lemonsLastCmd and not newgame:
# Calculate the weekly sales based on price and lowest inventory level
# (higher markup price = fewer sales, limited by the inventory on-hand)
sales = get_sales_amount(potential, unit, price)
@@ -478,7 +468,7 @@ def start_lemonade(nodeID, message, celsius=False):
msg += " N.Profit:" + locale.currency(net, grouping=True)
# Display the updated inventory levels
msg += "\nRemaining"
msg += f"\nRemaining"
msg += " 🥤:" + str(inventory.cups)
msg += " 🍋:" + str(inventory.lemons)
msg += " 🍚:" + str(inventory.sugar)
@@ -495,7 +485,7 @@ def start_lemonade(nodeID, message, celsius=False):
pad_week = len(str(weeks.total))
pad_sale = len(str(weeks.sales))
total = 0
msg += "\nWeekly📊"
msg += f"\nWeekly📊"
for i in range(len(weeks.summary)):
msg += "#" + str(weeks.current).rjust(pad_week) + ". " + str(weeks.summary[i]['sales']).rjust(pad_sale) + \
" sold x " + locale.currency(weeks.summary[i]['price'], grouping=True) + "ea. "
@@ -535,7 +525,7 @@ def start_lemonade(nodeID, message, celsius=False):
if (inventory.sugar <= 0):
msg += " You ran out of sugar.🍚"
else:
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
msg += f"\nCongratulations 🍋🍋 your sales were perfect!🎉"
# Increment the score counters
score.value = score.value + minnet
@@ -546,32 +536,32 @@ def start_lemonade(nodeID, message, celsius=False):
if (weeks.current == weeks.total):
# end of the game
success = round((score.value / score.total) * 100)
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
msg += f"\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
msg += f"\nYou've sold " + str(weeks.total_sales) + " total 🥤🍋"
# check for high score
high_score = getHighScoreLemon()
if (inventory.cash > int(high_score['cash'])):
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
msg += f"\nCongratulations! You've set a new high score!🎉💰🍋"
high_score['cash'] = inventory.cash
high_score['success'] = success
high_score['userID'] = nodeID
with open('data/lemonstand.pkl', 'wb') as file:
pickle.dump(high_score, file)
endGame(nodeID)
else:
# keep playing
weeks.current = weeks.current + 1
msg += f"\nPlay another week🥤? or (E)nd Game"
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "new"
lemonadeTracker[i]['time'] = time.time()
weeks.current = weeks.current + 1
msg += f"Play another week🥤? or (E)nd Game"
lemonadeTracker[i]['last_played'] = time.time()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
else:
return "Game Over! Start a (N)ew Game or (E)xit"

View File

@@ -5,9 +5,7 @@ import random
import time
import pickle
from modules.log import *
mindTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'secret_code': '', 'diff': 'n', 'turns': 1}]
from modules.settings import mindTracker
def chooseDifficultyMMind(message):
usrInput = message.lower()
msg = ''
@@ -62,98 +60,63 @@ def makeCodeMMind(diff):
return secret_code
#get guess from user
def getGuessMMind(diff, guess):
msg = ''
if diff == "n":
valid_colorsMMind = "RYGB"
elif diff == "h":
valid_colorsMMind = "RYGBOP"
elif diff == "x":
valid_colorsMMind = "RYGBOPWK"
user_guess = guess.upper()
valid_guess = True
if len(user_guess) != 4:
valid_guess = False
for i in range(len(user_guess)):
if user_guess[i] not in valid_colorsMMind:
valid_guess = False
if valid_guess == False:
user_guess = "XXXX"
def getGuessMMind(diff, guess, nodeID):
valid_colors = {
"n": "RYGB",
"h": "RYGBOP",
"x": "RYGBOPWK"
}
user_guess = guess.strip().upper()
if len(user_guess) != 4 or any(c not in valid_colors.get(diff, "RYGB") for c in user_guess):
return "XXXX"
#increase the turn count and store in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] += 1
mindTracker[i]['last_played'] = time.time()
mindTracker[i]['diff'] = diff
return user_guess
def getHighScoreMMind(nodeID, turns, diff):
# check if player is in high score list and pick the lowest score
try:
with open('mmind_hs.pkl', 'rb') as f:
mindHighScore = pickle.load(f)
except:
logger.debug("System: MasterMind: High Score file not found.")
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
import os
hs_file = 'data/mmind_hs.pkl'
# Try to load existing high scores
if os.path.exists(hs_file):
try:
with open(hs_file, 'rb') as f:
mindHighScore = pickle.load(f)
except Exception as e:
logger.debug(f"System: MasterMind: Error loading high score file: {e}")
mindHighScore = []
else:
mindHighScore = []
# If nodeID==0, just return 0
if nodeID == 0:
# just return the high score
mindHighScore = [{'nodeID': 0, 'turns': 0, 'diff': 'n'}]
return mindHighScore
# calculate lowest score
lowest_score = mindHighScore[0]['turns']
# If no high score, add this one
if not mindHighScore:
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
if mindHighScore[0]['diff'] == "n" and diff == "n":
if lowest_score > turns:
# update the high score for normal if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
# If the diff matches, compare and update if better
if mindHighScore[0]['diff'] == diff:
if turns < mindHighScore[0]['turns']:
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "n" and diff == "h":
# update the high score for hard if normal is the only high score
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "h" and diff == "h":
if lowest_score > turns:
# update the high score for hard if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "n" or mindHighScore[0]['diff'] == "h" and diff == "x":
# update the high score for expert if normal or high is the only high score
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "x" and diff == "x":
if lowest_score > turns:
# update the high score for expert if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
return 0
# If the diff is different, replace with new high score for new diff
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
def getEmojiMMind(secret_code):
@@ -182,7 +145,7 @@ def getEmojiMMind(secret_code):
return secret_code_emoji
#compare userGuess with secret code and provide feedback
def compareCodeMMind(secret_code, user_guess):
def compareCodeMMind(secret_code, user_guess, nodeID):
game_won = False
perfect_pins = 0
wrong_position = 0
@@ -210,9 +173,26 @@ def compareCodeMMind(secret_code, user_guess):
temp_code.remove(guess) # Remove the first occurrence of the matched color
# display feedback
if game_won:
msg += f"Correct{getEmojiMMind(user_guess)}\n"
msg += f"\n🏆Correct{getEmojiMMind(user_guess)}\nYou are the master mind!🤯"
# get turn count from tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
turns = mindTracker[i]['turns'] - 2 # subtract 2 to account for increment after last guess and starting at 1
diff = mindTracker[i]['diff']
# get high score
high_score = getHighScoreMMind(nodeID, turns, diff)
if high_score[0]['turns'] != 0:
msg += f"\n🏆 High Score:{turns} turns, Difficulty:{diff}"
# reset turn count in tracker
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 0
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
else:
msg += f"Guess{getEmojiMMind(user_guess)}\n"
msg += f"\nGuess{getEmojiMMind(user_guess)}\n"
if perfect_pins > 0 and game_won == False:
msg += "✅ color ✅ position: {}".format(perfect_pins)
@@ -231,11 +211,11 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
msg = ''
won = False
if turn_count <= 10:
user_guess = getGuessMMind(diff, message)
user_guess = getGuessMMind(diff, message, nodeID)
if user_guess == "XXXX":
msg += f"Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
return msg
check_guess = compareCodeMMind(secret_code, user_guess)
check_guess = compareCodeMMind(secret_code, user_guess, nodeID)
# display turn count and feedback
msg += "Turn {}:".format(turn_count)
@@ -245,18 +225,6 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
if won == True:
msg += f"\n🎉🧠 you win 🥷🤯"
# get high score
high_score = getHighScoreMMind(nodeID, turn_count, diff)
if high_score != 0:
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 1
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
else:
# increment turn count and keep playing
turn_count += 1
@@ -266,12 +234,12 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
mindTracker[i]['turns'] = turn_count
elif won == False:
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
msg += "\nYou have run out of turns.😿"
msg += "\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
msg += f"\nYou have run out of turns.😿"
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 1
mindTracker[i]['turns'] = 0
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'

165
modules/games/quiz.py Normal file
View File

@@ -0,0 +1,165 @@
# Quiz Module for meshbot 2025
# Provides a quiz game function with multiple choice and free-text questions
# Quizmaster can start/stop the quiz, players can join/leave, answer questions
# Scores are tracked, first correct answer is noted, top 3 players announced at end
# Questions are loaded from a JSON file in data/quiz_questions.json
# Questions can be multiple choice (with answers array) or free-text (with answer string)
# Players answer with "Q: <answer>" format, "Q: ?" for next question, locked to DM
# unlike a normal game, players can join/leave anytime during the quiz but the QuizMaster needs to start or open game
# Quizmaster can broadcast messages to all players
import json
import os
import random
from modules.log import *
QUIZ_JSON = os.path.join(os.path.dirname(__file__), '../', '../', 'data', 'quiz_questions.json')
QUIZMASTER_ID = bbs_admin_list
trap_list_quiz = ("quiz", "q:")
help_text_quiz = "quiz",
class QuizGame:
def __init__(self):
self.quizmaster = QUIZMASTER_ID
self.active = False
self.players = {} # user_id: {'score': int, 'current_q': int, 'answered': set()}
self.questions = [] # Loaded from JSON
self.first_correct = {} # q_idx: user_id
self.load_questions()
def start_game(self, quizmaster_id):
if str(quizmaster_id) not in self.quizmaster:
return "Only the quizmaster can start the quiz."
if self.active:
return "Quiz already running."
self.active = True
logger.debug(f"QuizMaster: {quizmaster_id} started a new quiz round.")
self.players = {}
self.first_correct = {} # Reset on new game
self.load_questions()
return "Quiz started! Players can now join."
def load_questions(self):
try:
with open(QUIZ_JSON, 'r') as f:
self.questions = json.load(f)
# Shuffle questions to ensure randomness each game
#random.shuffle(self.questions)
except Exception as e:
logger.error(f"Failed to load quiz questions: {e}")
self.questions = []
def stop_game(self, quizmaster_id):
if not self.active or str(quizmaster_id) not in self.quizmaster:
return "Only the quizmaster can stop the quiz."
return_msg = "Quiz stopped! Final scores:\n" + self.top_three()
logger.debug(f"QuizMaster: {quizmaster_id} stopped the quiz.")
self.active = False
self.players = {}
return return_msg
def join(self, user_id):
if not self.active:
return "No quiz running. Wait for the quizmaster to start."
if user_id in self.players:
return "You are already in the quiz."
self.players[user_id] = {'score': 0, 'current_q': 0, 'answered': set()}
reminder = f"Joined!\n'Q: <Answer>' 'Q: ?' for more.\n"
logger.debug(f"QuizMaster: Player {user_id} joined the round.")
return reminder + self.next_question(user_id)
def leave(self, user_id):
if user_id in self.players:
del self.players[user_id]
logger.debug(f"QuizMaster: Player {user_id} left the round.")
return "You left the quiz."
return "You are not in the quiz."
def next_question(self, user_id):
if user_id not in self.players:
return "Join the quiz first."
player = self.players[user_id]
while player['current_q'] < len(self.questions) and player['current_q'] in player['answered']:
player['current_q'] += 1
if player['current_q'] >= len(self.questions):
return f"No more questions. Your final score: {player['score']}."
q = self.questions[player['current_q']]
msg = f"Q{player['current_q']+1}: {q['question']}\n"
if "answers" in q:
for i, opt in enumerate(q['answers']):
msg += f"{chr(65+i)}. {opt}\n"
msg = msg.strip()
return msg
def answer(self, user_id, answer):
if user_id not in self.players:
return "Join the quiz first."
player = self.players[user_id]
q_idx = player['current_q']
if q_idx >= len(self.questions):
return "No more questions."
if q_idx in player['answered']:
return "Already answered. Type 'next' for another question."
q = self.questions[q_idx]
# Check if it's multiple choice or free-text
if "answers" in q and "correct" in q:
try:
ans_idx = ord(answer.upper()) - 65
if ans_idx == q['correct']:
player['score'] += 1
# Track first correct answer
if q_idx not in self.first_correct:
self.first_correct[q_idx] = user_id
logger.info(f"QuizMaster: Question {q_idx+1} first user with correct answer by {user_id}")
result = "Correct! 🎉"
else:
result = f"Wrong. Correct answer: {chr(65+q['correct'])}"
player['answered'].add(q_idx)
player['current_q'] += 1
return f"{result}\n" + self.next_question(user_id)
except Exception:
return "Invalid answer. Use A, B, C, etc."
elif "answer" in q:
user_ans = answer.strip().lower()
correct_ans = str(q['answer']).strip().lower()
if user_ans == correct_ans:
player['score'] += 1
if q_idx not in self.first_correct:
self.first_correct[q_idx] = user_id
logger.info(f"QuizMaster: Question {q_idx+1} first user with correct answer by {user_id}")
result = "Correct! 🎉"
else:
result = f"Wrong. Correct answer: {q['answer']}"
player['answered'].add(q_idx)
player['current_q'] += 1
return f"{result}\n" + self.next_question(user_id)
else:
return "Invalid question format."
def top_three(self):
if not self.players:
return "No players in the quiz."
ranking = sorted(self.players.items(), key=lambda x: x[1]['score'], reverse=True)
count = min(3, len(ranking))
msg = f"🏆 Top {count} Player{'s' if count > 1 else ''}:\n"
for idx, (uid, pdata) in enumerate(iterable=ranking[:count], start=1):
msg += f"{idx}. {uid}: @{pdata['score']}\n"
return msg
def broadcast(self, quizmaster_id, message):
msgToAll = {}
if quizmaster_id and str(quizmaster_id) not in self.quizmaster:
return "Only the quizmaster can broadcast."
if not self.players:
return "No players to broadcast to."
# set up message
message_to_send = f"📢 From Quizmaster: {message}"
msgToAll['message'] = message_to_send
# setup players
for uid in self.players.keys():
msgToAll.setdefault('players', []).append(uid)
return msgToAll
# Initialize the quiz game
quizGamePlayer = QuizGame()

277
modules/games/tictactoe.py Normal file
View File

@@ -0,0 +1,277 @@
# Tic-Tac-Toe game for Meshtastic mesh-bot
# Board positions chosen by numbers 1-9
# 2025
from modules.log import *
import random
import time
# to molly and jake, I miss you both so much.
if disable_emojis_in_games:
X = "X"
O = "O"
else:
X = ""
O = "⭕️"
class TicTacToe:
def __init__(self):
self.game = {}
def new_game(self, id):
positiveThoughts = ["🚀I need to call NATO",
"🏅Going for the gold!",
"Mastering ❌TTT⭕",]
sorryNotGoinWell = ["😭Not your day, huh?",
"📉Results here dont define you.",
"🤖WOPR would be proud."]
"""Start a new game"""
games = won = 0
ret = ""
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
if games > 3:
if won / games >= 3.14159265358979323846: # win rate > pi
ret += random.choice(positiveThoughts) + "\n"
else:
ret += random.choice(sorryNotGoinWell) + "\n"
# Retain stats
ret += f"Games:{games} 🥇❌:{won}\n"
self.game[id] = {
"board": [" "] * 9, # 3x3 board as flat list
"player": X, # Human is X, bot is O
"games": games + 1,
"won": won,
"turn": "human" # whose turn it is
}
ret += self.show_board(id)
ret += "Pick 1-9:"
return ret
def rndTeaPrice(self, tea=42):
"""Return a random tea between 0 and tea."""
return random.uniform(0, tea)
def show_board(self, id):
"""Display compact board with move numbers"""
g = self.game[id]
b = g["board"]
# Show board with positions
board_str = ""
for i in range(3):
row = ""
for j in range(3):
pos = i * 3 + j
if disable_emojis_in_games:
cell = b[pos] if b[pos] != " " else str(pos + 1)
else:
cell = b[pos] if b[pos] != " " else f" {str(pos + 1)} "
row += cell
if j < 2:
row += " | "
board_str += row
if i < 2:
#board_str += "\n-+-+-\n"
board_str += "\n"
return board_str + "\n"
def make_move(self, id, position):
"""Make a move for the current player"""
g = self.game[id]
# Validate position
if position < 1 or position > 9:
return False
pos = position - 1
if g["board"][pos] != " ":
return False
# Make human move
g["board"][pos] = X
return True
def bot_move(self, id):
"""AI makes a move: tries to win, block, or pick random"""
g = self.game[id]
board = g["board"]
# Try to win
move = self.find_winning_move(id, O)
if move != -1:
board[move] = O
return move
# Try to block player
move = self.find_winning_move(id, X)
if move != -1:
board[move] = O
return move
# Pick random move
move = self.find_random_move(id)
if move != -1:
board[move] = O
return move
# No moves possible
return -1
def find_winning_move(self, id, player):
"""Find a winning move for the given player"""
g = self.game[id]
board = g["board"][:]
# Check all empty positions
for i in range(9):
if board[i] == " ":
board[i] = player
if self.check_winner_on_board(board) == player:
return i
board[i] = " "
return -1
def find_random_move(self, id: str, tea_price: float = 42.0) -> int:
"""Find a random empty position, using time and tea_price for extra randomness."""
board = self.game[id]["board"]
empty = [i for i, cell in enumerate(board) if cell == " "]
current_time = time.time()
from_china = self.rndTeaPrice(time.time() % 7) # Correct usage
tea_price = from_china
tea_price = (42 * 7) - (13 / 2) + (tea_price % 5)
if not empty:
return -1
# Combine time and tea_price for a seed
seed = int(current_time * 1000) ^ int(tea_price * 1000)
local_random = random.Random(seed)
local_random.shuffle(empty)
return empty[0]
def check_winner_on_board(self, board):
"""Check winner on given board state"""
# Winning combinations
wins = [
[0,1,2], [3,4,5], [6,7,8], # Rows
[0,3,6], [1,4,7], [2,5,8], # Columns
[0,4,8], [2,4,6] # Diagonals
]
for combo in wins:
if board[combo[0]] == board[combo[1]] == board[combo[2]] != " ":
return board[combo[0]]
return None
def check_winner(self, id):
"""Check if there's a winner"""
g = self.game[id]
return self.check_winner_on_board(g["board"])
def is_board_full(self, id):
"""Check if board is full"""
g = self.game[id]
return " " not in g["board"]
def game_over_msg(self, id):
"""Generate game over message"""
g = self.game[id]
winner = self.check_winner(id)
if winner == X:
g["won"] += 1
return "🎉You won! (n)ew (e)nd"
elif winner == O:
return "🤖Bot wins! (n)ew (e)nd"
else:
return "🤝Tie, The only winning move! (n)ew (e)nd"
def play(self, id, input_msg):
"""Main game play function"""
if id not in self.game:
return self.new_game(id)
# If input is just "tictactoe", show current board
if input_msg.lower().strip() == ("tictactoe" or "tic-tac-toe"):
return self.show_board(id) + "Your turn! Pick 1-9:"
g = self.game[id]
# Parse player move
try:
# Extract just the number from the input
numbers = [char for char in input_msg if char.isdigit()]
if not numbers:
if input_msg.lower().startswith('q'):
self.end_game(id)
return "Game ended. To start a new game, type 'tictactoe'."
elif input_msg.lower().startswith('n'):
return self.new_game(id)
elif input_msg.lower().startswith('b'):
return self.show_board(id) + "Your turn! Pick 1-9:"
position = int(numbers[0])
except (ValueError, IndexError):
return "Enter 1-9, or (e)nd (n)ew game, send (b)oard to see board🧩"
# Make player move
if not self.make_move(id, position):
return "Invalid move! Pick 1-9:"
# Check if player won
if self.check_winner(id):
result = self.game_over_msg(id) + "\n" + self.show_board(id)
self.end_game(id)
return result
# Check for tie
if self.is_board_full(id):
result = self.game_over_msg(id) + "\n" + self.show_board(id)
self.end_game(id)
return result
# Bot's turn
bot_pos = self.bot_move(id)
# Check if bot won
if self.check_winner(id):
result = self.game_over_msg(id) + "\n" + self.show_board(id)
self.end_game(id)
return result
# Check for tie after bot move
if self.is_board_full(id):
result = self.game_over_msg(id) + "\n" + self.show_board(id)
self.end_game(id)
return result
# Continue game
return self.show_board(id) + "Your turn! Pick 1-9:"
def end_game(self, id):
"""Clean up finished game but keep stats"""
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
# Remove game but we'll create new one on next play
del self.game[id]
# Preserve stats for next game
self.game[id] = {
"board": [" "] * 9,
"player": X,
"games": games,
"won": won,
"turn": "human"
}
def end(self, id):
"""End game completely (called by 'end' command)"""
if id in self.game:
del self.game[id]
# Global instances for the bot system
tictactoeTracker = []
tictactoe = TicTacToe()

View File

@@ -6,8 +6,7 @@ import pickle
from modules.log import *
vpStartingCash = 20
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
from modules.settings import vpTracker
# Define the Card class
class CardVP:
@@ -164,7 +163,7 @@ class PlayerVP:
return self.show_hand()
except Exception as e:
pass
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
@@ -304,7 +303,7 @@ def playVideoPoker(nodeID, message):
# create new player if not in tracker
logger.debug(f"System: VideoPoker: New Player {nodeID}")
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
return f"Welcome to 🎰VideoPoker♥ you have {vpStartingCash} coins, Whats your bet?"
return f"You have {vpStartingCash} coins, \nWhats your bet?"
# Gather the player's bet
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
@@ -390,7 +389,7 @@ def playVideoPoker(nodeID, message):
else:
if drawCount <= 1:
msg = player.redraw(deck, message)
if msg.startswith("Send Card"):
if msg.startswith("ex:"):
# if returned error message, return it
return msg
drawCount += 1
@@ -403,7 +402,7 @@ def playVideoPoker(nodeID, message):
if drawCount == 2:
# this is the last draw will carry on to endGame for scoring
msg = player.redraw(deck, message) + f"\n"
if msg.startswith("Send Card"):
if msg.startswith("ex:"):
# if returned error message, return it
return msg
# redraw done
@@ -426,7 +425,7 @@ def playVideoPoker(nodeID, message):
if player.bankroll < 1:
player.bankroll = vpStartingCash
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
elif player.bankroll > vpTracker[i]['highScore']:
vpTracker[i]['highScore'] = player.bankroll
msg += " 🎉HighScore!"

77
modules/globalalert.py Normal file
View File

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

73
modules/gpio.py Normal file
View File

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

View File

@@ -1,34 +1,41 @@
#!/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 googlesearch import search # pip install googlesearch-python
import requests
import json
# 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
ragDEV = False
if ragDEV:
import os
import ollama # pip install ollama
import chromadb # pip install chromadb
if not rawLLMQuery:
# this may be removed in the future
from googlesearch import search # pip install googlesearch-python
# LLM System Variables
ollamaClient = OllamaClient(host=ollamaHostName)
ollamaAPI = ollamaHostName + "/api/generate"
tokens = 450 # max charcters for the LLM response, this is the max length of the response also in prompts
requestTruncation = True # if True, the LLM "will" truncate the response
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
# Used in the meshBotAI template
llmEnableHistory = True # enable last message history for the LLM model
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
antiFloodLLM = []
llmChat_history = {}
trap_list_llm = ("ask:", "askai")
meshbotAIinit = """
keep responses as short as possible. chatbot assistant no followuyp questions, no asking for clarification.
You must respond in plain text standard ASCII characters or emojis.
"""
truncatePrompt = f"truncate this as short as possible:\n"
meshBotAI = """
FROM {llmModel}
SYSTEM
@@ -41,7 +48,7 @@ meshBotAI = """
PROMPT
{input}
"""
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
@@ -69,78 +76,164 @@ if llmEnableHistory:
"""
def llm_readTextFiles():
# read .txt files in ../data/rag
# Tooling Functions Defined Here
# Example: current_time function
def llmTool_current_time():
"""
Example tool function to get the current time.
:return: Current time string.
"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')
def llmTool_math_calculator(expression):
"""
Example tool function to perform basic math calculations.
:param expression: A string containing a math expression (e.g., "2 + 2").
:return: The result of the calculation as a string.
"""
try:
text = []
directory = "../data/rag"
for filename in os.listdir(directory):
if filename.endswith(".txt"):
filepath = os.path.join(directory, filename)
with open(filepath, 'r') as f:
text.append(f.read())
return text
# WARNING: Using eval can be dangerous if not controlled properly.
# This is a simple example; in production, consider using a safe math parser.
result = eval(expression, {"__builtins__": None}, {})
return str(result)
except Exception as e:
logger.debug(f"System: LLM readTextFiles: {e}")
return False
return f"Error in calculation: {e}"
def store_text_embedding(text):
def llmTool_get_google(query, num_results=3):
"""
Example tool function to perform a Google search and return results.
:param query: The search query string.
:param num_results: Number of search results to return.
:return: A list of search result titles and descriptions.
"""
results = []
try:
# store each document in a vector embedding database
for i, d in enumerate(text):
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
embedding = response["embedding"]
collection.add(
ids=[str(i)],
embeddings=[embedding],
documents=[d]
)
googleSearch = search(query, advanced=True, num_results=num_results)
for result in googleSearch:
results.append(f"{result.title}: {result.description}")
return results
except Exception as e:
logger.debug(f"System: Embedding failed: {e}")
return False
return [f"Error in Google search: {e}"]
## INITALIZATION of RAG
if ragDEV:
llmFunctions = [
{
"name": "llmTool_current_time",
"description": "Get the current time.",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "llmTool_math_calculator",
"description": "Perform basic math calculations.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression to evaluate, e.g., '2 + 2'."
}
},
"required": ["expression"]
}
},
{
"name": "llmTool_get_google",
"description": "Perform a Google search and return results.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query string."
},
"num_results": {
"type": "integer",
"description": "Number of search results to return.",
"default": 3
}
},
"required": ["query"]
}
}
]
def get_google_context(input, num_results):
# Get context from Google search results
googleResults = []
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()
googleSearch = search(input, advanced=True, num_results=num_results)
if googleSearch:
for result in googleSearch:
googleResults.append(f"{result.title} {result.description}")
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())
googleResults = ['no other context provided']
except Exception as e:
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
return googleResults
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 send_ollama_query(llmQuery):
# Send the query to the Ollama API and return the response
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
if result.status_code == 200:
result_json = result.json()
result = result_json.get("response", "")
# deepseek has added <think> </think> tags to the response
if "<think>" in result:
result = result.split("</think>")[1]
else:
raise Exception(f"HTTP Error: {result.status_code}")
return result
def send_ollama_tooling_query(prompt, functions, model=None, max_tokens=450):
"""
Send a prompt and function/tool definitions to Ollama API for function calling.
:param prompt: The user prompt string.
:param functions: List of function/tool definitions (see Ollama API docs).
:param model: Model name (optional, defaults to llmModel).
:param max_tokens: Max tokens for response.
:return: Ollama API response JSON.
"""
if model is None:
model = llmModel
payload = {
"model": model,
"prompt": prompt,
"functions": functions,
"stream": False,
"max_tokens": max_tokens
}
result = requests.post(ollamaAPI, data=json.dumps(payload))
if result.status_code == 200:
return result.json()
else:
raise Exception(f"HTTP Error: {result.status_code} - {result.text}")
def llm_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
googleResults = []
# if this is the first initialization of the LLM the query of " " should bring meshbotAIinit OTA shouldnt reach this?
# This is for LLM like gemma and others now?
if input == " " and rawLLMQuery:
logger.warning("System: These LLM models lack a traditional system prompt, they can be verbose and not very helpful be advised.")
input = meshbotAIinit
else:
input = input.strip()
# classic model for gemma2, deepseek-r1, etc
logger.debug(f"System: Using classic LLM model framework, ideally for gemma2, deepseek-r1, etc")
if not location_name:
location_name = "no location provided "
# remove askai: and ask: from the input
for trap in trap_list_llm:
if input.lower().startswith(trap):
input = input[len(trap):].strip()
break
# add the naughty list here to stop the function before we continue
# add a list of allowed nodes only to use the function
@@ -151,24 +244,8 @@ def llm_query(input, nodeID=0, location_name=None):
else:
antiFloodLLM.append(nodeID)
if llmContext_fromGoogle:
# grab some context from the internet using google search hits (if available)
# localization details at https://pypi.org/project/googlesearch-python/
# remove common words from the search query
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
try:
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
if googleSearch:
for result in googleSearch:
# SearchResult object has url= title= description= just grab title and description
googleResults.append(f"{result.title} {result.description}")
else:
googleResults = ['no other context provided']
except Exception as e:
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
if llmContext_fromGoogle and not rawLLMQuery:
googleResults = get_google_context(input, googleSearchResults)
history = llmChat_history.get(nodeID, ["", ""])
@@ -182,34 +259,38 @@ def llm_query(input, nodeID=0, location_name=None):
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
# RAG context inclusion testing
ragContext = False
if ragDEV:
ragContext = query_collection(input)
if ragContext:
ragContextGooogle = ragContext + '\n'.join(googleResults)
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
# Query the model with RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
if rawLLMQuery:
# sanitize the input to remove tool call syntax
if '```' in input:
logger.warning("System: LLM Query: Code markdown detected, removing for raw query")
input = input.replace('```bash', '').replace('```python', '').replace('```', '')
modelPrompt = input
else:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# Query the model without RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
if isinstance(result, dict):
result = result.get("response")
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
# Query the model via Ollama web API
result = send_ollama_query(llmQuery)
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
antiFloodLLM.remove(nodeID) # Ensure removal on error
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', ' ')
if rawLLMQuery and requestTruncation and len(response) > 450:
#retryy loop to truncate the response
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
truncateResult = send_ollama_query(truncateQuery)
# cleanup for message output
response = result.strip().replace('\n', ' ')
# done with the query, remove the user from the anti flood list
antiFloodLLM.remove(nodeID)
@@ -217,15 +298,3 @@ def llm_query(input, nodeID=0, location_name=None):
llmChat_history[nodeID] = [input, response]
return response
# import subprocess
# def get_ollama_cpu():
# try:
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
# if "GPU" in psOutput.stdout:
# logger.debug(f"System: Ollama process with GPU")
# else:
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False

View File

@@ -1,4 +1,4 @@
# helper functions to use location data like NOAA weather
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
# K7MHI Kelly Keeton 2024
import json # pip install json
@@ -8,12 +8,15 @@ import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
import math
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar")
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
grid = mh.to_maiden(float(lat), float(lon))
location = lat, lon
if int(float(lat)) == 0 and int(float(lon)) == 0:
logger.error("Location: No GPS data, try sending location")
@@ -71,12 +74,24 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
def getRepeaterBook(lat=0, lon=0):
grid = mh.to_maiden(float(lat), float(lon))
data = []
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
# check if in the US or not
usapi ="https://www.repeaterbook.com/repeaters/prox_result.php?"
elsewhereapi = "https://www.repeaterbook.com/row_repeaters/prox2_result.php?"
if grid[:2] in ['CN', 'DN', 'EN', 'FN', 'CM', 'DM', 'EM', 'FM', 'DL', 'EL', 'FL']:
repeater_url = usapi
else:
repeater_url = elsewhereapi
repeater_url += f"city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
try:
msg = ''
response = requests.get(repeater_url)
user_agent = {'User-agent': 'Mozilla/5.0'}
response = requests.get(repeater_url, headers=user_agent, timeout=urlTimeoutSeconds)
if response.status_code!=200:
logger.error(f"Location:Error fetching repeater data from {repeater_url} with status code {response.status_code}")
soup = bs.BeautifulSoup(response.text, 'html.parser')
table = soup.find('table', attrs={'class': 'w3-table w3-striped w3-responsive w3-mobile w3-auto sortable'})
table = soup.find('table', attrs={'class': 'table table-striped table-hover align-middle sortable'})
if table is not None:
cells = table.find_all('td')
data = []
@@ -95,10 +110,8 @@ def getRepeaterBook(lat=0, lon=0):
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
}
data.append(repeater)
else:
msg = "bug?Not enough columns"
else:
msg = "bug?Table not found"
msg = "No Data for your Region"
except Exception as e:
msg = "No repeaters found 😔"
# Limit the output to the first 4 repeaters
@@ -120,6 +133,8 @@ def getArtSciRepeaters(lat=0, lon=0):
try:
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
response = requests.get(artsci_url)
if response.status_code!=200:
logger.error(f"Location:Error fetching data from {artsci_url} with status code {response.status_code}")
soup = bs.BeautifulSoup(response.text, 'html.parser')
# results needed xpath is /html/body/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table
table = soup.find_all('table')[1]
@@ -154,10 +169,10 @@ def getArtSciRepeaters(lat=0, lon=0):
else:
msg = f"no results.. sorry"
return msg
def get_tide(lat=0, lon=0):
def get_NOAAtide(lat=0, lon=0):
station_id = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, try sending location for tide")
return NO_DATA_NOGPS
@@ -172,7 +187,7 @@ def get_tide(lat=0, lon=0):
if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA
return "No tide station found with info provided"
station_id = station_json['stationList'][0]['stationId']
@@ -219,51 +234,53 @@ def get_tide(lat=0, lon=0):
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
def get_NOAAweather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
weather = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
unit = 1
logger.debug("Location: new API metric units not implemented yet")
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
if unit == 1:
weather_url += "&unit=1"
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
# extract the "forecast": property from the JSON response
try:
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
if not weather_data.ok:
logger.error("Location:Error fetching weather data from NOAA")
logger.warning("Location:Error fetching weather data from NOAA for location")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather data from NOAA")
logger.warning("Location:Error fetching weather data from NOAA for location")
return ERROR_FETCHING_DATA
# get the forecast URL from the JSON response
weather_json = weather_data.json()
forecast_url = weather_json['properties']['forecast']
try:
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
if not forecast_data.ok:
logger.warning("Location:Error fetching weather forecast from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching weather forecast from NOAA")
return ERROR_FETCHING_DATA
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
table = soup.find('div', id="detailed-forecast-body")
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
forecast_json = forecast_data.json()
forecast = forecast_json['properties']['periods']
for day in forecast[:forecastDuration]:
# abreviate the forecast
if table is None:
logger.error("Location:Bad weather data from NOAA")
return ERROR_FETCHING_DATA
else:
# get rows
rows = table.find_all('div', class_="row")
# extract data from rows
for row in rows:
# shrink the text
line = abbreviate_weather(row.text)
# only grab a few days of weather
if len(weather.split("\n")) < forecastDuration:
weather += line + "\n"
# trim off last newline
weather += abbreviate_noaa(day['name']) + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
# remove last newline
weather = weather[:-1]
# get any alerts and return the count
alerts = getWeatherAlerts(lat, lon)
alerts = getWeatherAlertsNOAA(lat, lon)
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
alert = ""
@@ -278,23 +295,43 @@ def get_weather(lat=0, lon=0, unit=0):
return weather
def abbreviate_weather(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 ",
def case_insensitive_replace(text, old, new):
"""Replace all occurrences of old (any case) in text with new."""
idx = 0
old_lower = old.lower()
text_lower = text.lower()
while True:
idx = text_lower.find(old_lower, idx)
if idx == -1:
break
text = text[:idx] + new + text[idx+len(old):]
text_lower = text.lower()
idx += len(new)
return text
def abbreviate_noaa(data=""):
# Long phrases (with spaces)
phrase_replacements = {
"less than a tenth of an inch possible": "< 0.1in",
"between a tenth and quarter of an inch possible": "0.1-0.25in",
"between a quarter and half an inch possible": "0.25-0.5in",
"between a half and three quarters of an inch possible": "0.5-0.75in",
"between one and two inches possible": "1-2in",
"between two and three inches possible": "2-3in",
"between three and four inches possible": "3-4in",
"between four and five inches possible": "4-5in",
"between five and six inches possible": "5-6in",
"between six and eight inches possible": "6-8in",
}
# Single words (no spaces)
word_replacements = {
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
"thursday": "Thu",
"friday": "Fri",
"saturday": "Sat",
"sunday": "Sun",
"northwest": "NW",
"northeast": "NE",
"southwest": "SW",
@@ -303,31 +340,54 @@ 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",
"accumulation": "accum",
"visibility": "vis",
"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.",
"temperatures": "temps:",
"temperature": "temp:",
"amounts": "amts:",
"afternoon": "Aftn",
"evening": "Eve",
}
line = row
for key, value in replacements.items():
line = line.replace(key, value)
return line
text = data
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
# Replace long phrases (case-insensitive)
for key in sorted(phrase_replacements, key=len, reverse=True):
value = phrase_replacements[key]
text = case_insensitive_replace(text, key, value)
# Replace single words (case-insensitive)
for key in word_replacements:
value = word_replacements[key]
text = case_insensitive_replace(text, key, value)
return text
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
@@ -350,11 +410,13 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
alerts = ""
alertxml = xml.dom.minidom.parseString(alert_data.text)
for i in alertxml.getElementsByTagName("entry"):
alerts += (
i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n"
)
title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue
area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue
if enableExtraLocationWx:
alerts += f"{title}. {area_desc.replace(' ', '')}\n"
else:
alerts += f"{title}\n"
if alerts == "" or alerts == None:
return NO_ALERTS
@@ -367,32 +429,42 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
alert_num = 0
alert_num = len(alerts.split("\n"))
alerts = abbreviate_weather(alerts)
alerts = abbreviate_noaa(alerts)
# return the first ALERT_COUNT alerts
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
return data
wxAlertCache = ""
def alertBrodcast():
wxAlertCacheNOAA = ""
def alertBrodcastNOAA():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
if currentAlert[0] == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
wxAlertCache = ""
global wxAlertCacheNOAA
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
# check if any reason to discard the alerts
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
return False
elif currentAlert == NO_ALERTS:
wxAlertCacheNOAA = ""
return False
if ignoreEASenable:
# check if the alert is in the ignoreEAS list
for word in ignoreEASwords:
if word.lower() in currentAlert[0].lower():
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
elif currentAlert[0] not in wxAlertCacheNOAA:
# Check if the current alert is not in the weather alert cache
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
wxAlertCacheNOAA = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.warning("Location:No GPS data, try sending location for weather alerts")
return NO_DATA_NOGPS
@@ -425,13 +497,13 @@ 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]
if alerts == "" or alerts == ['']:
return ERROR_FETCHING_DATA
return NO_ALERTS
# trim off last newline
if alerts[-1] == "\n":
@@ -440,3 +512,540 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
alerts = "\n".join(alerts)
return alerts
def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# get the latest IPAWS alert from FEMA
alert = ''
alerts = []
linked_data = ''
# set the API URL for IPAWS
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
# 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")
## state FIPS
## This logic is being added to reduce load on FEMA server.
stateFips = None
for cat in entry.getElementsByTagName("category"):
if cat.getAttribute("label") == "statefips":
stateFips = cat.getAttribute("term")
break
if stateFips is None:
# no stateFIPS found — skip
continue
# check if it matches your list
if stateFips not in myStateFIPSList:
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {myStateFIPSList}")
continue # skip to next entry
try:
# get the linked alert data from FEMA
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
if not linked_data.ok or not linked_data.text.strip():
# if the linked data is not ok, skip this alert
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
continue
else:
linked_xml = xml.dom.minidom.parseString(linked_data.text)
# this alert is a full CAP alert
except (requests.exceptions.RequestException):
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
continue
except xml.parsers.expat.ExpatError:
logger.warning(f"System: iPAWS Error parsing XML from {link}")
continue
except Exception as e:
logger.debug(f"System: iPAWS Error processing alert data from {link}: {e}")
continue
for info in linked_xml.getElementsByTagName("info"):
# only get en-US language alerts (alternative is es-US)
language_nodes = info.getElementsByTagName("language")
if not any(node.firstChild and node.firstChild.nodeValue.strip() == "en-US" for node in language_nodes):
continue # skip if not en-US
# extract values from XML
sameVal = "NONE"
geocode_value = "NONE"
description = ""
try:
eventCode_table = info.getElementsByTagName("eventCode")[0]
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
# use headline if no description
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
else:
description = headline
area_table = info.getElementsByTagName("area")[0]
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
geocode_table = area_table.getElementsByTagName("geocode")[0]
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
if geocode_type == "SAME":
sameVal = geocode_value
except Exception as e:
logger.debug(f"System: iPAWS Error extracting alert data: {link}")
#print(f"DEBUG: {info.toprettyxml()}")
continue
# check if the alert is for the SAME location, if wanted keep alert
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
# ignore the FEMA test alerts
if ignoreFEMAenable:
ignore_alert = False
for word in ignoreFEMAwords:
if word.lower() in headline.lower():
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
ignore_alert = True
break
if ignore_alert:
continue
# add to alert list
alerts.append({
'alertType': alertType,
'alertCode': alertCode,
'headline': headline,
'areaDesc': areaDesc,
'geocode_type': geocode_type,
'geocode_value': geocode_value,
'description': description
})
else:
logger.debug(f"System: iPAWS Alert not in SAME List: {sameVal} or {geocode_value} for {headline} at {areaDesc}")
continue
# return the numWxAlerts of alerts
if len(alerts) > 0:
for alertItem in alerts[:numWxAlerts]:
if shortAlerts:
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}")
else:
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}\n{alertItem['description']}")
# add a newline if not the last alert
if alertItem != alerts[:numWxAlerts][-1]:
alert += "\n"
else:
alert = NO_ALERTS
return alert
def get_flood_noaa(lat=0, lon=0, uid=None):
"""
Fetch the latest flood alert from NOAA for a given gauge UID.
Returns a formatted string or an error message.
"""
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
headers = {'accept': 'application/json'}
if not uid:
logger.warning(f"Location:No flood gauge data found for UID {uid}")
return ERROR_FETCHING_DATA
try:
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
if not response.ok:
logger.warning(f"Location:Error fetching flood gauge data from NOAA for {uid} (HTTP {response.status_code})")
return ERROR_FETCHING_DATA
data = response.json()
if not data or 'status' not in data:
logger.warning(f"Location:No flood gauge data found for UID {uid}")
return "No flood gauge data found"
except requests.exceptions.RequestException as e:
logger.warning(f"Location:Error fetching flood gauge data from: {api_url}{uid} ({e})")
return ERROR_FETCHING_DATA
except Exception as e:
logger.warning(f"Location:Unexpected error: {e}")
return ERROR_FETCHING_DATA
# extract values from JSON safely
try:
name = data.get('name', 'Unknown')
observed = data['status'].get('observed', {})
forecast = data['status'].get('forecast', {})
flood_data = f"Flood Data {name}:\n"
flood_data += f"Observed: {observed.get('primary', '?')}{observed.get('primaryUnit', '')} ({observed.get('secondary', '?')}{observed.get('secondaryUnit', '')}) risk: {observed.get('floodCategory', '?')}"
flood_data += f"\nForecast: {forecast.get('primary', '?')}{forecast.get('primaryUnit', '')} ({forecast.get('secondary', '?')}{forecast.get('secondaryUnit', '')}) risk: {forecast.get('floodCategory', '?')}"
#flood_data += f"\nStage: {data.get('stage', '?')} {data.get('stageUnit', '')}, Flow: {data.get('flow', '?')} {data.get('flowUnit', '')}"
#flood_data += f"\nLast Updated: {data.get('status', {}).get('lastUpdated', '?')}"
flood_data += f"\n"
return flood_data
except Exception as e:
logger.debug(f"Location:Error extracting flood gauge data from NOAA for {uid}: {e}")
return ERROR_FETCHING_DATA
def get_volcano_usgs(lat=0, lon=0):
alerts = ''
if lat == 0 and lon == 0:
lat = latitudeValue
lon = longitudeValue
# get the latest volcano alert from USGS from CAP feed
usgs_volcano_url = "https://volcanoes.usgs.gov/hans-public/api/volcano/getCapElevated"
try:
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
if not volcano_data.ok:
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
volcano_json = volcano_data.json()
# extract alerts from main feed
if volcano_json and isinstance(volcano_json, list):
for alert in volcano_json:
# check ignore list
if ignoreUSGSEnable:
for word in ignoreUSGSwords:
if word.lower() in alert['volcano_name_appended'].lower():
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
continue
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
volcano_name = alert['volcano_name_appended']
alert_level = alert['alert_level']
color_code = alert['color_code']
cap_severity = alert['cap_severity']
synopsis = alert['synopsis']
# format Alert
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
else:
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
continue
else:
logger.debug("Location:Error fetching volcano data from USGS")
return NO_ALERTS
if alerts == "":
return NO_ALERTS
# trim off last newline
if alerts[-1] == "\n":
alerts = alerts[:-1]
# return the alerts
alerts = abbreviate_noaa(alerts)
return alerts
def get_nws_marine(zone, days=3):
# forcast from NWS coastal products
try:
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
if not marine_pz_data.ok:
logger.warning("Location:Error fetching NWS Marine PZ data")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching NWS Marine PZ data")
return ERROR_FETCHING_DATA
marine_pz_data = marine_pz_data.text
#validate data
todayDate = datetime.now().strftime("%Y%m%d")
if marine_pz_data.startswith("Expires:"):
expires = marine_pz_data.split(";;")[0].split(":")[1]
expires_date = expires[:8]
if expires_date < todayDate:
logger.debug("Location: NWS Marine PZ data expired")
return ERROR_FETCHING_DATA
else:
logger.debug("Location: NWS Marine PZ data not valid")
return ERROR_FETCHING_DATA
# process the marine forecast data
marine_pzz_lines = marine_pz_data.split("\n")
marine_pz_report = ""
day_blocks = []
current_block = ""
in_forecast = False
for line in marine_pzz_lines:
if line.startswith(".") and "..." in line:
in_forecast = True
if current_block:
day_blocks.append(current_block.strip())
current_block = ""
current_block += line.strip() + " "
elif in_forecast and line.strip() != "":
current_block += line.strip() + " "
if current_block:
day_blocks.append(current_block.strip())
# Only keep up to pzDays blocks
for block in day_blocks[:days]:
marine_pz_report += block + "\n"
# remove last newline
if marine_pz_report.endswith("\n"):
marine_pz_report = marine_pz_report[:-1]
# remove NOAA EOF $$
if marine_pz_report.endswith("$$"):
marine_pz_report = marine_pz_report[:-2].strip()
# abbreviate the report
marine_pz_report = abbreviate_noaa(marine_pz_report)
if marine_pz_report == "":
return NO_DATA_NOGPS
return marine_pz_report
def checkUSGSEarthQuake(lat=0, lon=0):
if lat == 0 and lon == 0:
lat = latitudeValue
lon = longitudeValue
radius = 100 # km
magnitude = 1.5
history = 7 # days
startDate = datetime.fromtimestamp(datetime.now().timestamp() - history*24*60*60).strftime("%Y-%m-%d")
USGSquake_url = f"https://earthquake.usgs.gov/fdsnws/event/1/query?&format=xml&latitude={lat}&longitude={lon}&maxradiuskm={radius}&minmagnitude={magnitude}&starttime={startDate}"
description_text = ""
quake_count = 0
# fetch the earthquake data from USGS
try:
quake_data = requests.get(USGSquake_url, timeout=urlTimeoutSeconds)
if not quake_data.ok:
logger.warning("Location:Error fetching earthquake data from USGS")
return NO_ALERTS
if not quake_data.text.strip():
return NO_ALERTS
try:
quake_xml = xml.dom.minidom.parseString(quake_data.text)
except Exception as e:
logger.warning(f"Location: USGS earthquake API returned invalid XML: {e}")
return NO_ALERTS
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching earthquake data from USGS")
return NO_ALERTS
quake_xml = xml.dom.minidom.parseString(quake_data.text)
quake_count = len(quake_xml.getElementsByTagName("event"))
#get largest mag in magnitude of the set of quakes
largest_mag = 0.0
for event in quake_xml.getElementsByTagName("event"):
mag = event.getElementsByTagName("magnitude")[0]
mag_value = float(mag.getElementsByTagName("value")[0].childNodes[0].nodeValue)
if mag_value > largest_mag:
largest_mag = mag_value
# set description text
description_text = event.getElementsByTagName("description")[0].getElementsByTagName("text")[0].childNodes[0].nodeValue
largest_mag = round(largest_mag, 1)
if quake_count == 0:
return NO_ALERTS
else:
return f"{quake_count} 🫨quakes in last {history} days within {radius} km. Largest: {largest_mag}M\n{description_text}"
howfarDB = {}
def distance(lat=0,lon=0,nodeID=0, reset=False):
# part of the howfar function, calculates the distance between two lat/lon points
msg = ""
dupe = False
location = lat,lon
r = 6371 # Radius of earth in kilometers # haversine formula
if lat == 0 and lon == 0:
return NO_DATA_NOGPS
if nodeID == 0:
return "No NodeID provided"
if reset:
if nodeID in howfarDB:
del howfarDB[nodeID]
if nodeID not in howfarDB:
#register first point NodeID, lat, lon, time, point
howfarDB[nodeID] = [{'lat': lat, 'lon': lon, 'time': datetime.now()}]
if reset:
return "Tracking reset, new starting point registered🗺"
else:
return "Starting point registered🗺"
else:
#de-dupe points if same as last point
if howfarDB[nodeID][-1]['lat'] == lat and howfarDB[nodeID][-1]['lon'] == lon:
dupe = True
msg = "No New GPS📍 "
# calculate distance from last point in howfarDB
last_point = howfarDB[nodeID][-1]
lat1 = math.radians(last_point['lat'])
lon1 = math.radians(last_point['lon'])
lat2 = math.radians(lat)
lon2 = math.radians(lon)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
c = 2 * math.asin(math.sqrt(a))
distance_km = c * r
if use_metric:
msg += f"{distance_km:.2f} km"
else:
distance_miles = distance_km * 0.621371
msg += f"{distance_miles:.2f} miles"
#calculate bearing
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(dlon))
initial_bearing = math.atan2(x, y)
initial_bearing = math.degrees(initial_bearing)
compass_bearing = (initial_bearing + 360) % 360
msg += f" 🧭{compass_bearing:.2f}° Bearing from last📍"
# calculate the speed if time difference is more than 1 minute
time_diff = datetime.now() - last_point['time']
if time_diff.total_seconds() > 60:
hours = time_diff.total_seconds() / 3600
if use_metric:
speed = distance_km / hours
speed_str = f"{speed:.2f} km/h"
else:
speed_mph = (distance_km * 0.621371) / hours
speed_str = f"{speed_mph:.2f} mph"
msg += f", travel time: {int(time_diff.total_seconds()//60)} min, Speed: {speed_str}"
# calculate total distance traveled including this point computed in distance_km from calculate distance from last point in howfarDB
total_distance_km = 0.0
for i in range(1, len(howfarDB[nodeID])):
point1 = howfarDB[nodeID][i-1]
point2 = howfarDB[nodeID][i]
lat1 = math.radians(point1['lat'])
lon1 = math.radians(point1['lon'])
lat2 = math.radians(point2['lat'])
lon2 = math.radians(point2['lon'])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
c = 2 * math.asin(math.sqrt(a))
total_distance_km += c * r
# add the distance from last point to current point
total_distance_km += distance_km
if use_metric:
msg += f", Total: {total_distance_km:.2f} km"
else:
total_distance_miles = total_distance_km * 0.621371
msg += f", Total: {total_distance_miles:.2f} miles"
# update the last point in howfarDB
if not dupe:
howfarDB[nodeID].append({'lat': lat, 'lon': lon, 'time': datetime.now()})
# if points 3+ are within 30 meters of the first point add the area of the polygon
if len(howfarDB[nodeID]) >= 3:
points = []
# loop the howfarDB to get all the points except the current nodeID
for key in howfarDB:
if key != nodeID:
points.append((howfarDB[key][-1]['lat'], howfarDB[key][-1]['lon']))
# loop the howfarDB[nodeID] to get the points
for point in howfarDB[nodeID]:
points.append((point['lat'], point['lon']))
# close the polygon by adding the first point to the end
points.append((howfarDB[nodeID][0]['lat'], howfarDB[nodeID][0]['lon']))
# calculate the area of the polygon
area = 0.0
for i in range(len(points)-1):
lat1 = math.radians(points[i][0])
lon1 = math.radians(points[i][1])
lat2 = math.radians(points[i+1][0])
lon2 = math.radians(points[i+1][1])
area += (lon2 - lon1) * (2 + math.sin(lat1) + math.sin(lat2))
area = area * (6378137 ** 2) / 2.0
area = abs(area) / 1e6 # convert to square kilometers
if use_metric:
msg += f", Area: {area:.2f} sq.km (approx)"
else:
area_miles = area * 0.386102
msg += f", Area: {area_miles:.2f} sq.mi (approx)"
#calculate the centroid of the polygon
x = 0.0
y = 0.0
z = 0.0
for point in points[:-1]:
lat_rad = math.radians(point[0])
lon_rad = math.radians(point[1])
x += math.cos(lat_rad) * math.cos(lon_rad)
y += math.cos(lat_rad) * math.sin(lon_rad)
z += math.sin(lat_rad)
total_points = len(points) - 1
x /= total_points
y /= total_points
z /= total_points
lon_centroid = math.atan2(y, x)
hyp = math.sqrt(x * x + y * y)
lat_centroid = math.atan2(z, hyp)
lat_centroid = math.degrees(lat_centroid)
lon_centroid = math.degrees(lon_centroid)
msg += f", Centroid: {lat_centroid:.5f}, {lon_centroid:.5f}"
return msg
def get_openskynetwork(lat=0, lon=0):
# get the latest aircraft data from OpenSky Network in the area
if lat == 0 and lon == 0:
return NO_ALERTS
# setup a bounding box of 50km around the lat/lon
box_size = 0.45 # approx 50km
# return limits for aircraft search
search_limit = 3
lamin = lat - box_size
lamax = lat + box_size
lomin = lon - box_size
lomax = lon + box_size
# fetch the aircraft data from OpenSky Network
opensky_url = f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}&lamax={lamax}&lomax={lomax}"
try:
aircraft_data = requests.get(opensky_url, timeout=urlTimeoutSeconds)
if not aircraft_data.ok:
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching aircraft data from OpenSky Network")
return ERROR_FETCHING_DATA
aircraft_json = aircraft_data.json()
if 'states' not in aircraft_json or not aircraft_json['states']:
return NO_ALERTS
aircraft_list = aircraft_json['states']
aircraft_report = ""
for aircraft in aircraft_list:
if len(aircraft_report.split("\n")) >= search_limit:
break
# extract values from JSON
try:
callsign = aircraft[1].strip() if aircraft[1] else "N/A"
origin_country = aircraft[2]
velocity = aircraft[9]
true_track = aircraft[10]
vertical_rate = aircraft[11]
sensors = aircraft[12]
geo_altitude = aircraft[13]
squawk = aircraft[14] if len(aircraft) > 14 else "N/A"
except Exception as e:
logger.debug("Location:Error extracting aircraft data from OpenSky Network")
continue
# format the aircraft data
aircraft_report += f"{callsign} Alt:{int(geo_altitude) if geo_altitude else 'N/A'}m Vel:{int(velocity) if velocity else 'N/A'}m/s Heading:{int(true_track) if true_track else 'N/A'}°\n"
# remove last newline
if aircraft_report.endswith("\n"):
aircraft_report = aircraft_report[:-1]
aircraft_report = abbreviate_noaa(aircraft_report)
return aircraft_report if aircraft_report else NO_ALERTS

View File

@@ -1,8 +1,13 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from datetime import datetime, timedelta
from modules.settings import *
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
if not LOGGING_LEVEL:
LOGGING_LEVEL = "DEBUG"
LOGGING_LEVEL = getattr(logging, LOGGING_LEVEL)
class CustomFormatter(logging.Formatter):
grey = '\x1b[38;21m'
@@ -41,7 +46,7 @@ class plainFormatter(logging.Formatter):
# Create logger
logger = logging.getLogger("MeshBot System Logger")
logger.setLevel(logging.DEBUG)
logger.setLevel(LOGGING_LEVEL)
logger.propagate = False
msgLogger = logging.getLogger("MeshBot Messages Logger")
@@ -56,7 +61,7 @@ today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
# Set level for stdout handler (logs DEBUG level and above)
stdout_handler.setLevel(logging.DEBUG)
stdout_handler.setLevel(LOGGING_LEVEL)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Add handlers to the logger
@@ -64,14 +69,28 @@ logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
file_handler_sys.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)
msgLogger.addHandler(file_handler)
# Pretty Timestamp
def getPrettyTime(seconds):
# convert unix time to minutes, hours, days, or years for simple display
if seconds < 60:
return f"{int(seconds)}s"
elif seconds < 3600:
return f"{int(round(seconds / 60))}m"
elif seconds < 86400:
return f"{int(round(seconds / 3600))}h"
elif seconds < 31536000:
return f"{int(round(seconds / 86400))}d"
else:
return f"{int(round(seconds / 31536000))}y"

57
modules/qrz.py Normal file
View File

@@ -0,0 +1,57 @@
# Module to respomnd to new nodes we havent seen before with a hello message
# K7MHI Kelly Keeton 2024
import sqlite3
from modules.log import *
def initalize_qrz_database():
# create the database
conn = sqlite3.connect(qrz_db)
c = conn.cursor()
# Check if the qrz table exists, and create it if it doesn't
c.execute('''CREATE TABLE IF NOT EXISTS qrz
(qrz_id INTEGER PRIMARY KEY, qrz_call TEXT, qrz_name TEXT, qrz_qth TEXT, qrz_notes TEXT)''')
conn.commit()
conn.close()
def never_seen_before(nodeID):
# check if we have seen this node before and sent a hello message
conn = sqlite3.connect(qrz_db)
c = conn.cursor()
try:
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
row = c.fetchone()
conn.close()
if row is None:
# we have not seen this node before
return True
else:
# we have seen this node before
return False
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initalize_qrz_database()
logger.warning("QRZ database table not found, created new table")
# we have not seen this node before
return True
else:
raise
def hello(nodeID, name):
# send a hello message
conn = sqlite3.connect(qrz_db)
c = conn.cursor()
try:
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initalize_qrz_database()
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
else:
raise
conn.commit()
conn.close()
return True

View File

@@ -1,13 +1,105 @@
# meshing around with hamlib as a source for info to send to mesh network
# detect signal strength and frequency of active channel if appears to be in use send to mesh network
# depends on rigctld running externally as a network service
# also can use VOX detection with a microphone and vosk speech to text to send voice messages to mesh network
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
# 2024 Kelly Keeton K7MHI
import socket
import asyncio
from modules.log import *
import asyncio
# verbose debug logging for trap words function
debugVoxTmsg = False
if radio_detection_enabled:
# used by hamlib detection
import socket
if voxDetectionEnabled:
# methods available for trap word processing, these can be called by VOX detection when trap words are detected
from mesh_bot import tell_joke, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
botMethods = {
"joke": tell_joke,
"weather": handle_wxc,
"moon": handle_moon,
"daylight": handle_sun,
"river": handle_riverFlow,
"tide": handle_tide,
"satellite": handle_satpass}
# module global variables
previousVoxState = False
voxHoldTime = signalHoldTime
try:
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
from vosk import Model, KaldiRecognizer # pip install vosk
import json
q = asyncio.Queue(maxsize=32) # queue for audio data
if useLocalVoxModel:
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
else:
voxModel = Model(lang=voxLanguage) # use built in model for specified language
except Exception as e:
print(f"RadioMon: Error importing VOX dependencies: {e}")
print(f"To use VOX detection please install the vosk and sounddevice python modules")
print(f"pip install vosk sounddevice")
print(f"sounddevice needs pulseaudio, apt-get install portaudio19-dev")
voxDetectionEnabled = False
logger.error(f"RadioMon: VOX detection disabled due to import error")
FREQ_NAME_MAP = {
462562500: "GRMS CH1",
462587500: "GRMS CH2",
462612500: "GRMS CH3",
462637500: "GRMS CH4",
462662500: "GRMS CH5",
462687500: "GRMS CH6",
462712500: "GRMS CH7",
467562500: "GRMS CH8",
467587500: "GRMS CH9",
467612500: "GRMS CH10",
467637500: "GRMS CH11",
467662500: "GRMS CH12",
467687500: "GRMS CH13",
467712500: "GRMS CH14",
467737500: "GRMS CH15",
462550000: "GRMS CH16",
462575000: "GMRS CH17",
462600000: "GMRS CH18",
462625000: "GMRS CH19",
462675000: "GMRS CH20",
462670000: "GMRS CH21",
462725000: "GMRS CH22",
462725500: "GMRS CH23",
467575000: "GMRS CH24",
467600000: "GMRS CH25",
467625000: "GMRS CH26",
467650000: "GMRS CH27",
467675000: "GMRS CH28",
467700000: "FRS CH1",
462650000: "FRS CH5",
462700000: "FRS CH7",
462737500: "FRS CH16",
146520000: "2M Simplex Calling",
446000000: "70cm Simplex Calling",
156800000: "Marine CH16",
# Add more as needed
}
def get_freq_common_name(freq):
freq = int(freq)
name = FREQ_NAME_MAP.get(freq)
if name:
return name
else:
# Return MHz if not found
return f"{freq/1000000} Mhz"
def get_hamlib(msg="f"):
# get data from rigctld server
try:
rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
rigControlSocket.settimeout(2)
@@ -29,110 +121,47 @@ def get_hamlib(msg="f"):
except Exception as e:
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
return ERROR_FETCHING_DATA
def get_freq_common_name(freq):
freq = int(freq)
if freq == 462562500:
return "GRMS CH1"
elif freq == 462587500:
return "GRMS CH2"
elif freq == 462612500:
return "GRMS CH3"
elif freq == 462637500:
return "GRMS CH4"
elif freq == 462662500:
return "GRMS CH5"
elif freq == 462687500:
return "GRMS CH6"
elif freq == 462712500:
return "GRMS CH7"
elif freq == 467562500:
return "GRMS CH8"
elif freq == 467587500:
return "GRMS CH9"
elif freq == 467612500:
return "GRMS CH10"
elif freq == 467637500:
return "GRMS CH11"
elif freq == 467662500:
return "GRMS CH12"
elif freq == 467687500:
return "GRMS CH13"
elif freq == 467712500:
return "GRMS CH14"
elif freq == 467737500:
return "GRMS CH15"
elif freq == 462550000:
return "GRMS CH16"
elif freq == 462575000:
return "GMRS CH17"
elif freq == 462600000:
return "GMRS CH18"
elif freq == 462625000:
return "GMRS CH19"
elif freq == 462675000:
return "GMRS CH20"
elif freq == 462670000:
return "GMRS CH21"
elif freq == 462725000:
return "GMRS CH22"
elif freq == 462725500:
return "GMRS CH23"
elif freq == 467575000:
return "GMRS CH24"
elif freq == 467600000:
return "GMRS CH25"
elif freq == 467625000:
return "GMRS CH26"
elif freq == 467650000:
return "GMRS CH27"
elif freq == 467675000:
return "GMRS CH28"
elif freq == 467700000:
return "FRS CH1"
elif freq == 462575000:
return "FRS CH2"
elif freq == 462600000:
return "FRS CH3"
elif freq == 462650000:
return "FRS CH5"
elif freq == 462675000:
return "FRS CH6"
elif freq == 462700000:
return "FRS CH7"
elif freq == 462725000:
return "FRS CH8"
elif freq == 462562500:
return "FRS CH9"
elif freq == 462587500:
return "FRS CH10"
elif freq == 462612500:
return "FRS CH11"
elif freq == 462637500:
return "FRS CH12"
elif freq == 462662500:
return "FRS CH13"
elif freq == 462687500:
return "FRS CH14"
elif freq == 462712500:
return "FRS CH15"
elif freq == 462737500:
return "FRS CH16"
elif freq == 146520000:
return "2M Simplex Calling"
elif freq == 446000000:
return "70cm Simplex Calling"
elif freq == 156800000:
return "Marine CH16"
else:
#return Mhz
freq = freq/1000000
return f"{freq} Mhz"
def get_sig_strength():
strength = get_hamlib('l STRENGTH')
return strength
def checkVoxTrapWords(text):
try:
if not voxOnTrapList:
logger.debug(f"RadioMon: VOX detected: {text}")
return text
if text:
traps = [voxTrapList] if isinstance(voxTrapList, str) else voxTrapList
text_lower = text.lower()
for trap in traps:
trap_clean = trap.strip()
trap_lower = trap_clean.lower()
idx = text_lower.find(trap_lower)
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX checking for trap word '{trap_lower}' in: '{text}' (index: {idx})")
if idx != -1:
new_text = text[idx + len(trap_clean):].strip()
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX detected trap word '{trap_lower}' in: '{text}' (remaining: '{new_text}')")
new_words = new_text.split()
if voxEnableCmd:
for word in new_words:
if word in botMethods:
logger.info(f"RadioMon: VOX action '{word}' with '{new_text}'")
if word == "joke":
return botMethods[word](vox=True)
else:
return botMethods[word](None, None, None, vox=True)
logger.debug(f"RadioMon: VOX returning text after trap word '{trap_lower}': '{new_text}'")
return new_text
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX no trap word found in: '{text}'")
return None
except Exception as e:
logger.debug(f"RadioMon: Error in checkVoxTrapWords: {e}")
return None
async def signalWatcher():
global previousStrength
global signalCycle
@@ -157,4 +186,61 @@ async def signalWatcher():
signalCycle = 0
previousStrength = -40
# end of file
async def make_vox_callback(loop, q):
def vox_callback(indata, frames, time, status):
if status:
logger.warning(f"RadioMon: VOX input status: {status}")
try:
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
except asyncio.QueueFull:
# Drop the oldest item and add the new one
try:
q.get_nowait() # Remove oldest
except asyncio.QueueEmpty:
pass
try:
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
except asyncio.QueueFull:
# If still full, just drop this frame
logger.debug("RadioMon: VOX queue full, dropping audio frame")
except RuntimeError:
# Loop may be closed
pass
return vox_callback
async def voxMonitor():
global previousVoxState, voxMsgQueue
try:
model = voxModel
device_info = sd.query_devices(voxInputDevice, 'input')
samplerate = 16000
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
rec = KaldiRecognizer(model, samplerate)
loop = asyncio.get_running_loop()
callback = await make_vox_callback(loop, q)
with sd.RawInputStream(
device=voxInputDevice,
samplerate=samplerate,
blocksize=4000,
dtype='int16',
channels=1,
callback=callback
):
while True:
data = await q.get()
if rec.AcceptWaveform(data):
result = rec.Result()
text = json.loads(result).get("text", "")
# process text
if text and text != 'huh':
result = checkVoxTrapWords(text)
if result:
# If result is a function return, handle it (send to mesh, log, etc.)
# If it's just text, handle as a normal message
voxMsgQueue.append(result)
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"RadioMon: Error in VOX monitor: {e}")
# end of file

95
modules/rss.py Normal file
View File

@@ -0,0 +1,95 @@
# rss feed module for meshing-around 2025
from modules.log import *
import urllib.request
import xml.etree.ElementTree as ET
import html
from html.parser import HTMLParser
class MLStripper(HTMLParser):
def __init__(self):
super().__init__()
self.reset()
self.fed = []
def handle_data(self, d):
self.fed.append(d)
def get_data(self):
return ''.join(self.fed)
def strip_tags(html_text):
s = MLStripper()
s.feed(html_text)
return s.get_data()
RSS_FEED_URLS = rssFeedURL
RSS_FEED_NAMES = rssFeedNames
RSS_RETURN_COUNT = rssMaxItems
RSS_TRIM_LENGTH = rssTruncate
def get_rss_feed(msg):
# Determine which feed to use
feed_name = ""
msg_lower = msg.lower() if msg else ""
if msg_lower and any(name.lower() in msg_lower for name in RSS_FEED_NAMES):
for name in RSS_FEED_NAMES:
if name.lower() in msg_lower:
feed_name = name
break
else:
logger.debug(f"RSS: No feed name found in message '{msg}'. Using default feed.")
feed_name = RSS_FEED_NAMES[0] if RSS_FEED_NAMES else "default"
try:
idx = RSS_FEED_NAMES.index(feed_name)
feed_url = RSS_FEED_URLS[idx]
except (ValueError, IndexError):
logger.warning(f"RSS: Feed '{feed_name}' not found in RSS_FEED_URLS ({RSS_FEED_URLS}).")
return f"Feed '{feed_name}' not found."
if "?" in msg_lower:
return f"Fetches the latest {RSS_RETURN_COUNT} entries RSS feeds. Available feeds are: {', '.join(RSS_FEED_NAMES)}. To fetch a specific feed, include its name in your request."
try:
logger.debug(f"Fetching RSS feed from {feed_url} from message '{msg}'")
agent = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
request = urllib.request.Request(feed_url, headers=agent)
with urllib.request.urlopen(request, timeout=urlTimeoutSeconds) as response:
xml_data = response.read()
root = ET.fromstring(xml_data)
# Try both namespaced and non-namespaced item tags
items = root.findall('.//item')
ns = None
if not items:
# Try to find the namespace dynamically
for elem in root.iter():
if elem.tag.endswith('item'):
ns_uri = elem.tag.split('}')[0].strip('{')
items = root.findall(f'.//{{{ns_uri}}}item')
ns = ns_uri
break
items = items[:RSS_RETURN_COUNT]
if not items:
return "No RSS feed entries found."
formatted_entries = []
for item in items:
if ns:
title = item.findtext(f'{{{ns}}}title', default='No title')
link = item.findtext(f'{{{ns}}}link', default=None)
description = item.findtext(f'{{{ns}}}description', default='No description')
pub_date = item.findtext(f'{{{ns}}}pubDate', default='No date')
else:
title = item.findtext('title', default='No title')
link = item.findtext('link', default=None)
description = item.findtext('description', default='No description')
pub_date = item.findtext('pubDate', default='No date')
# Unescape HTML entities and strip tags
description = html.unescape(description)
description = strip_tags(description)
if len(description) > RSS_TRIM_LENGTH:
description = description[:RSS_TRIM_LENGTH - 3] + "..."
formatted_entries.append(f"{title}\n{description}\n")
return "\n".join(formatted_entries)
except Exception as e:
logger.error(f"Error fetching RSS feed from {feed_url}: {e}")
return ERROR_FETCHING_DATA

118
modules/scheduler.py Normal file
View File

@@ -0,0 +1,118 @@
# modules/scheduler.py 2025 meshing-around
# Scheduler setup for Mesh Bot
import asyncio
import schedule
from modules.log import logger
from modules.system import send_message
async def setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
# methods available for custom scheduler messages
from mesh_bot import tell_joke, welcome_message, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
schedulerValue = schedulerValue.lower().strip()
schedulerTime = schedulerTime.strip()
schedulerInterval = schedulerInterval.strip()
schedulerChannel = int(schedulerChannel)
schedulerInterface = int(schedulerInterface)
# Setup the scheduler based on configuration
try:
if schedulerMotd:
scheduler_message = MOTD
else:
scheduler_message = schedulerMessage
# Basic Scheduler Options
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
# Basic scheduler job to run the schedule see examples below for custom schedules
if schedulerValue.lower() == 'day':
if schedulerTime != '':
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
else:
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'hour' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'min' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'joke' in schedulerValue.lower():
# Schedule to send a joke every specified interval
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'weather' in schedulerValue.lower():
# Schedule to send weather updates every specified interval
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'custom' in schedulerValue.lower():
# Custom scheduler job to run the schedule see examples below
# custom scheduler job to run the schedule see examples below
logger.debug(f"System: Starting the custom scheduler default to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
# send a joke every 15 minutes
#schedule.every(15).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Place your custom schedule code below this line, helps with merges
# Place your custom schedule code above this line
# Start the Broadcast Scheduler
await BroadcastScheduler()
except Exception as e:
logger.error(f"System: Scheduler Error {e}")
# Enhanced Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", schedulerChannel, 0, schedulerInterface)).day(15, 25)
# Send a joke every 6 hours
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Send a joke every 2 minutes
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Send the Welcome Message every other day at 08:00
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, schedulerChannel, 0, schedulerInterface))
# Send the MOTD every day at 13:00
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, schedulerChannel, 0, schedulerInterface))
# Send bbslink looking for peers every other day at 10:00
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))

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
@@ -20,69 +21,109 @@ ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 4 # default retry count for interfaces
retry_int1 = False
retry_int2 = False
scheduler_enabled = False # enable the scheduler currently config via code only
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
cmdHistory = [] # list to hold the command history for lheard and history commands
msg_history = [] # list to hold the message history for the messages command
max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content
voxMsgQueue = [] # queue for VOX detected messages
# Game trackers
surveyTracker = [] # Survey game tracker
tictactoeTracker = [] # TicTacToe game tracker
hamtestTracker = [] # Ham radio test tracker
hangmanTracker = [] # Hangman game tracker
golfTracker = [] # GolfSim game tracker
mastermindTracker = [] # Mastermind game tracker
vpTracker = [] # Video Poker game tracker
jackTracker = [] # Blackjack game tracker
lemonadeTracker = [] # Lemonade Stand game tracker
dwPlayerTracker = [] # DopeWars player tracker
jackTracker = [] # Jack game tracker
mindTracker = [] # Mastermind (mmind) game tracker
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
config_file = "config.ini"
try:
config.read(config_file)
config.read(config_file, encoding='utf-8')
except Exception as e:
print(f"System: Error reading config file: {e}")
# exit if we can't read the config file
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")
exit(1)
if config.sections() == []:
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD,
'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config.write(open(config_file, 'w'))
print (f"System: Config file created, check {config_file} or review the config.template")
if 'sentry' not in config:
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
if 'repeater' not in config:
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
if 'radioMon' not in config:
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
if 'games' not in config:
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
if 'messagingSettings' not in config:
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
if 'fileMon' not in config:
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
if 'scheduler' not in config:
config['scheduler'] = {'enabled': 'False'}
config.write(open(config_file, 'w'))
if 'emergencyHandler' not in config:
config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''}
config.write(open(config_file, 'w'))
if 'smtp' not in config:
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
config.write(open(config_file, 'w'))
if 'checklist' not in config:
config['checklist'] = {'enabled': 'False', 'checklist_db': 'data/checklist.db'}
config.write(open(config_file, 'w'))
if 'qrz' not in config:
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
hostname1 = config['interface'].get('hostname', '')
mac1 = config['interface'].get('mac', '')
interface1_enabled = True # gotta have at least one interface
# interface2 settings
if 'interface2' in config:
@@ -94,98 +135,306 @@ if 'interface2' in config:
else:
interface2_enabled = False
# variables
# interface3 settings
if 'interface3' in config:
interface3_type = config['interface3'].get('type', 'serial')
port3 = config['interface3'].get('port', '')
hostname3 = config['interface3'].get('hostname', '')
mac3 = config['interface3'].get('mac', '')
interface3_enabled = config['interface3'].getboolean('enabled', False)
else:
interface3_enabled = False
# interface4 settings
if 'interface4' in config:
interface4_type = config['interface4'].get('type', 'serial')
port4 = config['interface4'].get('port', '')
hostname4 = config['interface4'].get('hostname', '')
mac4 = config['interface4'].get('mac', '')
interface4_enabled = config['interface4'].getboolean('enabled', False)
else:
interface4_enabled = False
# interface5 settings
if 'interface5' in config:
interface5_type = config['interface5'].get('type', 'serial')
port5 = config['interface5'].get('port', '')
hostname5 = config['interface5'].get('hostname', '')
mac5 = config['interface5'].get('mac', '')
interface5_enabled = config['interface5'].getboolean('enabled', False)
else:
interface5_enabled = False
# interface6 settings
if 'interface6' in config:
interface6_type = config['interface6'].get('type', 'serial')
port6 = config['interface6'].get('port', '')
hostname6 = config['interface6'].get('hostname', '')
mac6 = config['interface6'].get('mac', '')
interface6_enabled = config['interface6'].getboolean('enabled', False)
else:
interface6_enabled = False
# interface7 settings
if 'interface7' in config:
interface7_type = config['interface7'].get('type', 'serial')
port7 = config['interface7'].get('port', '')
hostname7 = config['interface7'].get('hostname', '')
mac7 = config['interface7'].get('mac', '')
interface7_enabled = config['interface7'].getboolean('enabled', False)
else:
interface7_enabled = False
# interface8 settings
if 'interface8' in config:
interface8_type = config['interface8'].get('type', 'serial')
port8 = config['interface8'].get('port', '')
hostname8 = config['interface8'].get('hostname', '')
mac8 = config['interface8'].get('mac', '')
interface8_enabled = config['interface8'].getboolean('enabled', False)
else:
interface8_enabled = False
# interface9 settings
if 'interface9' in config:
interface9_type = config['interface9'].get('type', 'serial')
port9 = config['interface9'].get('port', '')
hostname9 = config['interface9'].get('hostname', '')
mac9 = config['interface9'].get('mac', '')
interface9_enabled = config['interface9'].getboolean('enabled', False)
else:
interface9_enabled = False
multiple_interface = False
if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled:
multiple_interface = True
# variables from the config.ini file
try:
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
cmdBang = config['general'].getboolean('cmdBang', False) # default off
explicitCmd = config['general'].getboolean('explicitCmd', True) # default on
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
urlTimeoutSeconds = config['general'].getint('urlTimeout', 15) # default 15 seconds for URL fetch timeout
store_forward_enabled = config['general'].getboolean('StoreForward', True)
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
reverseSF = config['general'].getboolean('reverseSF', False) # default False, send oldest first
welcome_message = config['general'].get('welcome_message', WELCOME_MSG)
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
motd_enabled = config['general'].getboolean('motdEnabled', True)
MOTD = config['general'].get('motd', MOTD)
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
whoami_enabled = config['general'].getboolean('whoami', True)
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
bee_enabled = config['general'].getboolean('bee', False) # 🐝 off by default undocumented
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
use_kiwix_server = config['general'].getboolean('useKiwixServer', False)
kiwix_url = config['general'].get('kiwixURL', 'http://127.0.0.1:8080')
kiwix_library_name = config['general'].get('kiwixLibraryName', 'wikipedia_en_100_nopic_2024-06')
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
llmModel = config['general'].get('ollamaModel', 'gemma3:270m') # default gemma3:270m
rawLLMQuery = config['general'].getboolean('rawLLMQuery', True) #default True
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True) # default True
dont_retry_disconnect = config['general'].getboolean('dont_retry_disconnect', False) # default False, retry on disconnect
favoriteNodeList = config['general'].get('favoriteNodeList', '').split(',')
enableEcho = config['general'].getboolean('enableEcho', False) # default False
echoChannel = config['general'].getint('echoChannel', '9') # default 9, empty string to ignore
rssEnable = config['general'].getboolean('rssEnable', True) # default True
rssFeedURL = config['general'].get('rssFeedURL', 'http://www.hackaday.com/rss.xml,https://www.arrl.org/rss/arrl.rss').split(',')
rssMaxItems = config['general'].getint('rssMaxItems', 3) # default 3 items
rssTruncate = config['general'].getint('rssTruncate', 100) # default 100 characters
rssFeedNames = config['general'].get('rssFeedNames', 'default,arrl').split(',')
# 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
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
highfly_interface = config['sentry'].getint('highFlyingAlertInterface', 1) # default 1
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
# location
location_enabled = config['location'].getboolean('enabled', True)
latitudeValue = config['location'].getfloat('lat', 48.50)
longitudeValue = config['location'].getfloat('lon', -123.0)
fuzz_config_location = config['location'].getboolean('fuzzConfigLocation', True) # default True
fuzzItAll = config['location'].getboolean('fuzzAllLocations', False) # default False, only fuzz config location
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
riverListDefault = config['location'].get('riverList', '').split(',') # default None
coastalEnabled = config['location'].getboolean('coastalEnabled', False) # default False
myCoastalZone = config['location'].get('myCoastalZone', None) # default None
coastalForecastDays = config['location'].getint('coastalForecastDays', 3) # default 3 days
# location alerts
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if wxAlertBroadcastChannel:
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
myStateFIPSList = config['location'].get('myFIPSList', '').split(',') # default empty
mySAMEList = config['location'].get('mySAMEList', '').split(',') # default empty
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
volcanoAlertBroadcastChannel = config['location'].get('volcanoAlertBroadcastCh', '2').split(',') # default Channel 2
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
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(',')
bbsAPI_enabled = config['bbs'].getboolean('bbsAPI_enabled', False)
# checklist
checklist_enabled = config['checklist'].getboolean('enabled', False)
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
# qrz hello
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
train_qrz = config['qrz'].getboolean('training', True)
# E-Mail Settings
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
enableImap = config['smtp'].getboolean('enableImap', False)
SMTP_SERVER = config['smtp'].get('SMTP_SERVER', 'smtp.gmail.com')
SMTP_PORT = config['smtp'].getint('SMTP_PORT', 587)
FROM_EMAIL = config['smtp'].get('FROM_EMAIL', 'none@gmail.com')
SMTP_AUTH = config['smtp'].getboolean('SMTP_AUTH', True)
SMTP_USERNAME = config['smtp'].get('SMTP_USERNAME', FROM_EMAIL)
SMTP_PASSWORD = config['smtp'].get('SMTP_PASSWORD', 'password')
EMAIL_SUBJECT = config['smtp'].get('EMAIL_SUBJECT', 'Meshtastic✉')
IMAP_SERVER = config['smtp'].get('IMAP_SERVER', 'imap.gmail.com')
IMAP_PORT = config['smtp'].getint('IMAP_PORT', 993)
IMAP_USERNAME = config['smtp'].get('IMAP_USERNAME', SMTP_USERNAME)
IMAP_PASSWORD = config['smtp'].get('IMAP_PASSWORD', SMTP_PASSWORD)
IMAP_FOLDER = config['smtp'].get('IMAP_FOLDER', 'inbox')
# repeater
repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
# scheduler
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
schedulerInterval = config['scheduler'].get('interval', '') # default empty
schedulerTime = config['scheduler'].get('time', '') # default empty
schedulerValue = config['scheduler'].get('value', '') # default empty
schedulerMotd = config['scheduler'].getboolean('schedulerMotd', False) # default False
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
sigWatchBroadcastInterface = config['radioMon'].getint('sigWatchBroadcastInterface', 1) # default interface 1
signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm
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
voxDetectionEnabled = config['radioMon'].getboolean('voxDetectionEnabled', False) # default VOX detection disabled
voxDescription = config['radioMon'].get('voxDescription', 'VOX') # default VOX detected audio message
useLocalVoxModel = config['radioMon'].getboolean('useLocalVoxModel', False) # default False
localVoxModelPath = config['radioMon'].get('localVoxModelPath', 'no') # default models/vox.tflite
voxLanguage = config['radioMon'].get('voxLanguage', 'en-US') # default en-US
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
voxEnableCmd = config['radioMon'].getboolean('voxEnableCmd', True) # default True
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', '../data/news.txt') # default ../data/news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
allowXcmd = config['fileMon'].getboolean('allowXcmd', False) # default False
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', True) # default True
xCmd2factor_timeout = config['fileMon'].getint('2factor_timeout', 100) # default 100 seconds
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
game_hop_limit = config['games'].getint('game_hop_limit', 5) # default 5 hops
disable_emojis_in_games = config['games'].getboolean('disable_emojis', False) # default False
dopewars_enabled = config['games'].getboolean('dopeWars', True)
lemonade_enabled = config['games'].getboolean('lemonade', True)
blackjack_enabled = config['games'].getboolean('blackjack', True)
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
mastermind_enabled = config['games'].getboolean('mastermind', True)
golfSim_enabled = config['games'].getboolean('golfSim', True)
hangman_enabled = config['games'].getboolean('hangman', True)
hamtest_enabled = config['games'].getboolean('hamtest', True)
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
quiz_enabled = config['games'].getboolean('quiz', False)
survey_enabled = config['games'].getboolean('survey', False)
default_survey = config['games'].get('defaultSurvey', 'example') # default example
surveyRecordID = config['games'].getboolean('surveyRecordID', True)
surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', 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
except KeyError as e:
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160 chars
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 bytes
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
DEBUGpacket = config['messagingSettings'].getboolean('DEBUGpacket', False) # default False
noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False
logMetaStats = config['messagingSettings'].getboolean('logMetaStats', True) # default True
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
except Exception as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")

272
modules/smtp.py Normal file
View File

@@ -0,0 +1,272 @@
# 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 the nodeID has over 5 sms addresses warn and return
for item in sms_db:
if item['nodeID'] == nodeID:
if len(item['sms']) >= 5:
logger.warning("System: 📵SMS limit reached for " + str(nodeID))
return False
# if not in db, add it
if nodeID not in sms_db:
sms_db.append({'nodeID': nodeID, 'sms': sms})
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

@@ -1,142 +0,0 @@
# helper functions to get HF band conditions, DRAP X-ray flux, and sunrise/sunset times
# HF code from https://github.com/Murturtle/MeshLink
# K7MHI Kelly Keeton 2024
import requests # pip install requests
import xml.dom.minidom
from datetime import datetime
import ephem # pip install pyephem
from datetime import timedelta
from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond")
def hf_band_conditions():
# ham radio HF band conditions
hf_cond = ""
band_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(band_cond.ok):
solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
hf_cond = hf_cond[:-1] # remove the last newline
else:
logger.error("Solar: Error fetching HF band conditions")
hf_cond = ERROR_FETCHING_DATA
return hf_cond
def solar_conditions():
# radio related solar conditions from hamsql.com
solar_cond = ""
solar_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(solar_cond.ok):
solar_xml = xml.dom.minidom.parseString(solar_cond.text)
for i in solar_xml.getElementsByTagName("solardata"):
solar_a_index = i.getElementsByTagName("aindex")[0].childNodes[0].data
solar_k_index = i.getElementsByTagName("kindex")[0].childNodes[0].data
solar_xray = i.getElementsByTagName("xray")[0].childNodes[0].data
solar_flux = i.getElementsByTagName("solarflux")[0].childNodes[0].data
sunspots = i.getElementsByTagName("sunspots")[0].childNodes[0].data
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
else:
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond
def drap_xray_conditions():
# DRAP X-ray flux conditions, from NOAA direct
drap_cond = ""
drap_cond = requests.get("https://services.swpc.noaa.gov/text/drap_global_frequencies.txt", timeout=urlTimeoutSeconds)
if(drap_cond.ok):
drap_list = drap_cond.text.split('\n')
x_filter = '# X-RAY Message :'
for line in drap_list:
if x_filter in line:
xray_flux = line.split(": ")[1]
else:
logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux
def get_sun(lat=0, lon=0):
# get sunrise and sunset times using callers location or default
obs = ephem.Observer()
obs.date = datetime.now()
sun = ephem.Sun()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
sun.compute(obs)
sun_table = {}
sun_table['azimuth'] = sun.az
sun_table['altitude'] = sun.alt
# get the next rise and set times
local_sunrise = ephem.localtime(obs.next_rising(sun))
local_sunset = ephem.localtime(obs.next_setting(sun))
if zuluTime:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %H:%M')
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
else:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %I:%M%p')
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
# if sunset is before sunrise, then it's tomorrow
if local_sunset < local_sunrise:
local_sunset = ephem.localtime(obs.next_setting(sun)) + timedelta(1)
if zuluTime:
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
else:
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
return sun_data
def get_moon(lat=0, lon=0):
# get moon phase and rise/set times using callers location or default
# the phase calculation mght not be accurate (followup later)
obs = ephem.Observer()
moon = ephem.Moon()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
obs.date = datetime.now()
moon.compute(obs)
moon_table = {}
moon_phase = ['NewMoon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous', 'FullMoon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent'][round(moon.phase / (2 * ephem.pi) * 8) % 8]
moon_table['phase'] = moon_phase
moon_table['illumination'] = moon.phase
moon_table['azimuth'] = moon.az
moon_table['altitude'] = moon.alt
local_moonrise = ephem.localtime(obs.next_rising(moon))
local_moonset = ephem.localtime(obs.next_setting(moon))
if zuluTime:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %H:%M')
moon_table['set_time'] = local_moonset.strftime('%a %d %H:%M')
else:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %I:%M%p')
moon_table['set_time'] = local_moonset.strftime('%a %d %I:%M%p')
local_next_full_moon = ephem.localtime(ephem.next_full_moon((obs.date)))
local_next_new_moon = ephem.localtime(ephem.next_new_moon((obs.date)))
if zuluTime:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %H:%M')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %H:%M')
else:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %I:%M%p')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %I:%M%p')
moon_data = "MoonRise:" + moon_table['rise_time'] + "\nSet:" + moon_table['set_time'] + \
"\nPhase:" + moon_table['phase'] + " @:" + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
return moon_data

252
modules/space.py Normal file
View File

@@ -0,0 +1,252 @@
# helper functions to get HF band conditions, DRAP X-ray flux, and sunrise/sunset times
# HF code from https://github.com/Murturtle/MeshLink
# K7MHI Kelly Keeton 2024
import requests # pip install requests
import xml.dom.minidom
from datetime import datetime
import ephem # pip install pyephem
from datetime import timezone
from modules.log import *
import math
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass", "howtall")
def hf_band_conditions():
# ham radio HF band conditions
hf_cond = ""
signalnoise = ""
band_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(band_cond.ok):
solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
hf_cond = hf_cond[:-1] # remove the last newline
for i in solarxml.getElementsByTagName("solardata"):
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
hf_cond += "\nQRN:" + signalnoise
else:
logger.error("Solar: Error fetching HF band conditions")
hf_cond = ERROR_FETCHING_DATA
return hf_cond
def solar_conditions():
# radio related solar conditions from hamsql.com
solar_cond = ""
solar_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(solar_cond.ok):
solar_xml = xml.dom.minidom.parseString(solar_cond.text)
for i in solar_xml.getElementsByTagName("solardata"):
solar_a_index = i.getElementsByTagName("aindex")[0].childNodes[0].data
solar_k_index = i.getElementsByTagName("kindex")[0].childNodes[0].data
solar_xray = i.getElementsByTagName("xray")[0].childNodes[0].data
solar_flux = i.getElementsByTagName("solarflux")[0].childNodes[0].data
sunspots = i.getElementsByTagName("sunspots")[0].childNodes[0].data
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
else:
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond
def drap_xray_conditions():
# DRAP X-ray flux conditions, from NOAA direct
drap_cond = ""
drap_cond = requests.get("https://services.swpc.noaa.gov/text/drap_global_frequencies.txt", timeout=urlTimeoutSeconds)
if(drap_cond.ok):
drap_list = drap_cond.text.split('\n')
x_filter = '# X-RAY Message :'
for line in drap_list:
if x_filter in line:
xray_flux = line.split(": ")[1]
else:
logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux
def get_sun(lat=0, lon=0):
# get sunrise and sunset times using callers location or default
obs = ephem.Observer()
obs.date = datetime.now(timezone.utc)
sun = ephem.Sun()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
sun.compute(obs)
sun_table = {}
# get the sun azimuth and altitude
sun_table['azimuth'] = sun.az
sun_table['altitude'] = sun.alt
# sun is up include altitude
if sun_table['altitude'] > 0:
sun_table['altitude'] = sun.alt
else:
sun_table['altitude'] = 0
# get the next rise and set times
local_sunrise = ephem.localtime(obs.next_rising(sun))
local_sunset = ephem.localtime(obs.next_setting(sun))
if zuluTime:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %H:%M')
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
else:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %I:%M%p')
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
# if sunset is before sunrise, then data will be for tomorrow format sunset first and sunrise second
if local_sunset < local_sunrise:
sun_data = "SunSet: " + sun_table['set_time'] + "\nRise: " + sun_table['rise_time']
else:
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
sun_data += "\nDaylight: " + str((local_sunset - local_sunrise).seconds // 3600) + "h " + str(((local_sunset - local_sunrise).seconds // 60) % 60) + "m"
if sun_table['altitude'] > 0:
sun_data += "\nRemaining: " + str((local_sunset - datetime.now()).seconds // 3600) + "h " + str(((local_sunset - datetime.now()).seconds // 60) % 60) + "m"
sun_data += "\nAzimuth: " + str('{0:.2f}'.format(sun_table['azimuth'] * 180 / ephem.pi)) + "°"
if sun_table['altitude'] > 0:
sun_data += "\nAltitude: " + str('{0:.2f}'.format(sun_table['altitude'] * 180 / ephem.pi)) + "°"
return sun_data
def get_moon(lat=0, lon=0):
# get moon phase and rise/set times using callers location or default
obs = ephem.Observer()
moon = ephem.Moon()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
obs.date = datetime.now(timezone.utc)
moon.compute(obs)
moon_table = {}
illum = moon.phase # 0 = new, 50 = first/last quarter, 100 = full
if illum < 1.0:
moon_phase = 'New Moon🌑'
elif illum < 49:
moon_phase = 'Waxing Crescent 🌒'
elif 49 <= illum < 51:
moon_phase = 'First Quarter 🌓'
elif illum < 99:
moon_phase = 'Waxing Gibbous 🌔'
elif illum >= 99:
moon_phase = 'Full Moon🌕'
elif illum > 51:
moon_phase = 'Waning Gibbous 🌖'
elif 51 >= illum > 49:
moon_phase = 'Last Quarter 🌗'
else:
moon_phase = 'Waning Crescent 🌘'
moon_table['phase'] = moon_phase
moon_table['illumination'] = moon.phase
moon_table['azimuth'] = moon.az
moon_table['altitude'] = moon.alt
local_moonrise = ephem.localtime(obs.next_rising(moon))
local_moonset = ephem.localtime(obs.next_setting(moon))
if zuluTime:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %H:%M')
moon_table['set_time'] = local_moonset.strftime('%a %d %H:%M')
else:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %I:%M%p')
moon_table['set_time'] = local_moonset.strftime('%a %d %I:%M%p')
local_next_full_moon = ephem.localtime(ephem.next_full_moon((obs.date)))
local_next_new_moon = ephem.localtime(ephem.next_new_moon((obs.date)))
if zuluTime:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %H:%M')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %H:%M')
else:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %I:%M%p')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %I:%M%p')
moon_data = "MoonRise: " + moon_table['rise_time'] + "\nSet: " + moon_table['set_time'] + \
"\nPhase: " + moon_table['phase'] + " @: " + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
+ "\nFullMoon: " + moon_table['next_full_moon'] + "\nNewMoon: " + moon_table['next_new_moon']
# if moon is in the sky, add azimuth and altitude
if moon_table['altitude'] > 0:
moon_data += "\nAz: " + str('{0:.2f}'.format(moon_table['azimuth'] * 180 / ephem.pi)) + "°" + \
"\nAlt: " + str('{0:.2f}'.format(moon_table['altitude'] * 180 / ephem.pi)) + "°"
return moon_data
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
def measureHeight(lat=0, lon=0, shadow=0):
# measure height of a given location using sun angle and shadow length
if lat == 0 and lon == 0:
return NO_DATA_NOGPS
if shadow == 0:
return NO_ALERTS
obs = ephem.Observer()
obs.lat = str(lat)
obs.lon = str(lon)
obs.date = datetime.now(timezone.utc)
sun = ephem.Sun()
sun.compute(obs)
sun_altitude = sun.alt * 180 / ephem.pi
if sun_altitude <= 0:
return "Sun is below horizon, I dont belive your shadow measurement"
try:
if use_metric:
height = float(shadow) * math.tan(sun.alt)
return f"📏Object Height: {height:.2f} m (Shadow: {shadow} m, 📐Sun Alt: {sun_altitude:.2f}°)"
else:
# Assume shadow is in feet if imperial, otherwise convert from meters to feet
shadow_ft = float(shadow)
height_ft = shadow_ft * math.tan(sun.alt)
return f"📏Object Height: {height_ft:.2f} ft (Shadow: {shadow_ft} ft, 📐Sun Alt: {sun_altitude:.2f}°)"
except Exception as e:
logger.error(f"Space: Error calculating height: {e}")
return NO_ALERTS

196
modules/survey.py Normal file
View File

@@ -0,0 +1,196 @@
# Survey Module for meshbot 2025
# Provides a survey function to collect responses and put into a CSV file
# this module reads survey definitions from JSON files in the data/surveys directory
# Each survey is defined in a separate JSON file named <survey_name>_survey.json
# Example survey file: example_survey.json
# Example survey response file: example_responses.csv
# Each survey consists of multiple questions, which can be multiple choice, integer, or text
# Users can start a survey, answer questions, and end the survey
# Module acts like a game locking DM until the survey is complete or ended
import json
import os # For file operations
from collections import Counter
from modules.log import *
allowedSurveys = [] # List of allowed survey names
trap_list_survey = ("survey",)
class SurveyModule:
def __init__(self):
self.base_dir = os.path.dirname(__file__)
self.survey_dir = os.path.join(self.base_dir, '..', 'data', 'surveys') # Directory for survey JSON files
self.response_dir = os.path.join(self.base_dir, '..', 'data', 'surveys') # Directory for survey response CSV files
self.surveys = {}
self.responses = {}
self.load_surveys()
def load_surveys(self):
"""Load all surveys from the surveys directory with _survey.json suffix."""
global allowedSurveys
allowedSurveys.clear()
try:
for filename in os.listdir(self.survey_dir):
if filename.endswith('_survey.json'):
survey_name = filename[:-12] # Remove '_survey.json'
allowedSurveys.append(survey_name)
path = os.path.join(self.survey_dir, filename)
try:
with open(path, encoding='utf-8') as f:
self.surveys[survey_name] = json.load(f)
except FileNotFoundError:
logger.error(f"File not found: {path}")
self.surveys[survey_name] = []
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from file: {path}")
self.surveys[survey_name] = []
except Exception as e:
logger.error(f"Survey: Error loading surveys: {e}")
def start_survey(self, user_id, survey_name='example', location=None):
try:
"""Begin a new survey session for a user."""
if not survey_name:
survey_name = default_survey
if survey_name not in allowedSurveys:
return f"error: survey '{survey_name}' is not allowed."
self.responses[user_id] = {
'survey_name': survey_name,
'current_question': 0,
'answers': [],
'location': location if surveyRecordLocation and location is not None else 'N/A'
}
msg = f"'{survey_name}'📝survey\n"
msg += self.show_question(user_id)
msg += f"\nSend answer' or 'end'"
return msg
except Exception as e:
logger.error(f"Error starting survey for user {user_id}: {e}")
return "An error occurred while starting the survey. Please try again later."
def show_question(self, user_id):
"""Show the current question for the user, or end the survey."""
survey_name = self.responses[user_id]['survey_name']
current = self.responses[user_id]['current_question']
questions = self.surveys.get(survey_name, [])
if current >= len(questions):
return self.end_survey(user_id)
question = questions[current]
msg = f"{question['question']}\n"
if question.get('type', 'multiple_choice') == 'multiple_choice':
for i, option in enumerate(question['options']):
msg += f"{chr(65+i)}. {option}\n"
elif question['type'] == 'integer':
msg += "(Please enter a number)\n"
elif question['type'] == 'text':
msg += "(Please enter your response)\n"
msg = msg.rstrip('\n')
return msg
def save_responses(self, user_id):
"""Save user responses to a CSV file."""
survey_name = self.responses[user_id]['survey_name']
if survey_name not in self.surveys:
logger.warning(f"Survey '{survey_name}' not loaded. Responses not saved.")
return
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
try:
with open(filename, 'a', encoding='utf-8') as f:
# Always write: timestamp, userID, position, answers...
timestamp = datetime.datetime.now().strftime('%d%m%Y%H%M%S')
user_id_str = str(user_id)
location = self.responses[user_id].get('location', "N/A")
answers = list(map(str, self.responses[user_id]['answers']))
row = [timestamp, user_id_str, str(location)] + answers
f.write(','.join(row) + '\n')
logger.info(f"Survey: Responses for user {user_id} saved for survey '{survey_name}' to {filename}.")
except Exception as e:
logger.error(f"Error saving responses to {filename}: {e}")
def answer(self, user_id, answer, location=None):
try:
"""Record an answer and return the next question or end message."""
if user_id not in self.responses:
return self.start_survey(user_id, location=location)
question_index = self.responses[user_id]['current_question']
survey_name = self.responses[user_id]['survey_name']
questions = self.surveys.get(survey_name, [])
if question_index < 0 or question_index >= len(questions):
return "No current question to answer."
question = questions[question_index]
qtype = question.get('type', 'multiple_choice')
if qtype == 'multiple_choice':
answer_char = answer.strip().upper()[:1]
if len(answer_char) != 1 or not answer_char.isalpha():
return "Please answer with a letter (A, B, C, ...)."
option_index = ord(answer_char) - 65
if 0 <= option_index < len(question['options']):
# Valid answer record letter, not index
self.responses[user_id]['answers'].append(answer_char)
self.responses[user_id]['current_question'] += 1
return f"Recorded..\n" + self.show_question(user_id)
else:
print(f"Invalid option index {option_index} for question with {len(question['options'])} options. user entered '{answer}'")
return "Invalid answer option. Please try again."
elif qtype == 'integer':
try:
int_answer = int(answer)
self.responses[user_id]['answers'].append(str(int_answer))
self.responses[user_id]['current_question'] += 1
return f"Recorded..\n" + self.show_question(user_id)
except ValueError:
return "Please enter a valid integer."
elif qtype == 'text':
self.responses[user_id]['answers'].append(answer.strip())
self.responses[user_id]['current_question'] += 1
return f"Recorded..\n" + self.show_question(user_id)
else:
return f"error: unknown question type '{qtype}' and cannot record answer '{answer}'"
except Exception as e:
logger.error(f"Error recording answer for user {user_id}: {e}")
return "An error occurred while recording your answer. Please try again."
def end_survey(self, user_id):
"""End the survey for the user and save responses."""
if user_id not in self.responses:
return "No active survey session to end."
self.save_responses(user_id)
self.responses.pop(user_id, None)
return "✅ Survey complete. Thank you for your responses!"
def quiz_report(self, survey_name='example'):
"""
Generate a quick poll report: counts of each answer per question.
Returns a string summary.
"""
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
questions = self.surveys.get(survey_name, [])
if not questions:
logger.warning(f"No survey found for '{survey_name}'.")
return f"No survey found for '{survey_name}'."
all_answers = []
try:
with open(filename, encoding='utf-8') as f:
for line in f:
parts = line.strip().split(',')
if surveyRecordID:
answers = [int(x) for x in parts[1:] if x.strip().isdigit()]
else:
answers = [int(x) for x in parts if x.strip().isdigit()]
all_answers.append(answers)
except FileNotFoundError:
logger.info(f"No responses recorded yet for '{survey_name}'.")
return "No responses recorded yet."
report = f"📊 Poll Report for '{survey_name}':\n"
for q_idx, question in enumerate(questions):
counts = Counter(ans[q_idx] for ans in all_answers if len(ans) > q_idx)
report += f"\nQ{q_idx+1}: {question['question']}\n"
for opt_idx, option in enumerate(question.get('options', [])):
count = counts.get(opt_idx, 0)
report += f" {chr(65+opt_idx)}. {option}: {count}\n"
return report
# Initialize the survey module
survey_module = SurveyModule()

File diff suppressed because it is too large Load Diff

126
modules/udp.py Normal file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# UDP Interface Listener
# credit to pdxlocations for all of this core work https://github.com/pdxlocations/
# depends on: pip install meshtastic protobuf zeroconf pubsub
# 2025 Kelly Keeton K7MHI
from pubsub import pub
from meshtastic.protobuf import mesh_pb2, portnums_pb2
from mudp import UDPPacketStream, node, conn, send_text_message, send_nodeinfo, send_device_telemetry, send_position, send_environment_metrics, send_power_metrics, send_waypoint, send_data
from mudp.encryption import generate_hash
import time
from zeroconf import Zeroconf, ServiceBrowser
import socket
MCAST_GRP, MCAST_PORT, CHANNEL_ID, KEY = "224.0.0.69", 4403, "LongFast", "1PG7OiApB1nwvP+rz05pAQ=="
PUBLIC_CHANNEL_IDS = ["LongFast", "ShortSlow", "MediumFast", "MediumSlow", "ShortFast", "ShortTurbo"]
mudpEnabled, mudpInterface = True, None
messages = []
class ZeroconfListner:
def add_service(self, zeroconf, type, name):
info = zeroconf.get_service_info(type, name)
if info:
txt = info.properties
ip = None
if info.addresses:
ip = socket.inet_ntoa(info.addresses[0])
print(f"Found Meshtastic node: id={txt.get(b'id', b'').decode()} shortname={txt.get(b'shortname', b'').decode()} longname={txt.get(b'longname', b'').decode()} ip={ip}")
def update_service(self, zeroconf, type, name):
# This method is required by zeroconf, but you can leave it empty if you don't need updates.
pass
def initalize_mudp():
global mudpInterface
if mudpEnabled and mudpInterface is None:
mudpInterface = UDPPacketStream(MCAST_GRP, MCAST_PORT, key=KEY)
print(f"MUDP Interface initialized with multicast group", MCAST_GRP, "port", MCAST_PORT)
node.node_id, node.long_name, node.short_name = "!deadbeef", "UDP Test", "UDP"
node.channel, node.key = "LongFast", KEY
conn.setup_multicast(MCAST_GRP, MCAST_PORT)
def on_recieve(packet: mesh_pb2.MeshPacket, addr=None):
print(f"\n[RECV] Packet received from {addr}")
print("from:", getattr(packet, "from", None))
print("to:", packet.to)
# Check against all public channels
matched_channel = None
for channel_name in PUBLIC_CHANNEL_IDS:
channel_hash = generate_hash(channel_name, KEY)
if packet.channel == channel_hash:
matched_channel = channel_name
break
if matched_channel:
channel_status = f"Match ({matched_channel})"
else:
channel_status = f"Hash: {packet.channel}"
print("channel:", channel_status)
if packet.HasField("decoded"):
port_name = portnums_pb2.PortNum.Name(packet.decoded.portnum) if packet.decoded.portnum else "N/A"
try:
payload_decoded = True
packet_payload = packet.decoded.payload.decode("utf-8", "ignore")
except Exception:
print(" payload (raw bytes):", packet.decoded.payload)
else:
print(f"encrypted: { {packet.encrypted} }")
print("id:", packet.id or None)
print("rx_time:", packet.rx_time or None)
print("rx_snr:", packet.rx_snr or None)
print("hop_limit:", packet.hop_limit or None)
priority_name = mesh_pb2.MeshPacket.Priority.Name(packet.priority) if packet.priority else "N/A"
print("priority:", priority_name or None)
print("rx_rssi:", packet.rx_rssi or None)
print("hop_start:", packet.hop_start or None)
print("next_hop:", packet.next_hop or None)
print("relay_node:", packet.relay_node or None)
print(f"decoded {{portnum: {port_name}, payload: {packet_payload if payload_decoded else 'N/A'}, bitfield: {packet.decoded.bitfield or None}}}" if packet.HasField("decoded") else "No decoded field")
pub.subscribe(on_recieve, "mesh.rx.packet")
# pub.subscribe(on_text_message, "mesh.rx.port.1")
# pub.subscribe(on_nodeinfo, "mesh.rx.port.4") # NODEINFO_APP
zeroconf = Zeroconf()
listener = ZeroconfListner()
browser = ServiceBrowser(zeroconf, "_meshtastic._tcp.local.", listener)
def main():
initalize_mudp()
mudpInterface.start()
try:
while True: time.sleep(0.05)
except KeyboardInterrupt: pass
finally: mudpInterface.stop()
if __name__ == "__main__":
main()
# Meshtastic Port Numbers Reference:
# | Port Number | Name | Purpose |
# |-------------|------------------------|--------------------------------|
# | 1 | TEXT_MESSAGE_APP | Text messages |
# | 2 | POSITION_APP | Position updates (GPS) |
# | 3 | ROUTING_APP | Routing info |
# | 4 | NODEINFO_APP | Node info (name, id, etc) |
# | 5 | TELEMETRY_APP | Telemetry (battery, sensors) |
# | 6 | SERIAL_APP | Serial data |
# | 7 | ENVIRONMENTAL_APP | Environmental sensors |
# | 8 | REMOTE_HARDWARE_APP | Remote hardware control |
# | 9 | STORE_FORWARD_APP | Store and forward |
# | 10 | RANGE_TEST_APP | Range test |
# | 11 | ADMIN_APP | Admin/config |
# | 12 | WAYPOINT_APP | Waypoints |
# | 13 | CHANNEL_NODEINFO_APP | Channel node info |
# | 256 | PRIVATE_APP | Private app (custom use) |
# See: https://github.com/meshtastic/protobufs/blob/main/meshtastic/protobuf/portnums.proto

62
modules/web.py Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# This is a simple web server that serves up the content of the webRoot directory
# The reporting data is all that is currently being served up
# TODO - add interaction to mesh?
# to use this today run it seperately and open a browser to http://localhost:8420
import os
import http.server
# Set the desired IP address
server_ip = '127.0.0.1'
# Set the port for the server
PORT = 8420
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
SSL = False
# Set to True to enable logging sdtout
webServerLogs = False
# Determine the directory where this script is located.
script_dir = os.path.dirname(os.path.realpath(__file__))
# Go up one level from the modules directory to the project root.
project_root = os.path.abspath(os.path.join(script_dir, ".."))
# Build the absolute path to the webRoot folder; to where index.html is located.
webRoot = os.path.join(project_root, "etc", "www")
if SSL:
import ssl
# disable logging
class QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
if webServerLogs:
super().log_message(format, *args)
# Change the current working directory to webRoot
os.chdir(webRoot)
# Create the HTTP server instance with the desired IP address
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
if SSL:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
try:
ctx.load_cert_chain(certfile='./server.pem')
except FileNotFoundError:
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
exit(1)
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print(f"Serving reports at https://{server_ip}:{PORT} Press ^C to quit.\n\n")
else:
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
if not webServerLogs:
print("Server Logs are disabled")
# Serve forever, that is until the user interrupts the process
httpd.serve_forever()
exit(0)

122
modules/wiki.py Normal file
View File

@@ -0,0 +1,122 @@
# meshbot wiki module
from modules.log import *
import wikipedia # pip install wikipedia
# Kiwix support for local wiki
if use_kiwix_server:
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
from bs4.element import Comment
# Kiwix helper functions (only loaded if use_kiwix_server is True)
if wikipedia_enabled and use_kiwix_server:
def tag_visible(element):
"""Filter visible text from HTML elements for Kiwix"""
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
return False
if isinstance(element, Comment):
return False
return True
def text_from_html(body):
"""Extract visible text from HTML content"""
soup = BeautifulSoup(body, 'html.parser')
texts = soup.find_all(string=True)
visible_texts = filter(tag_visible, texts)
return " ".join(t.strip() for t in visible_texts if t.strip())
def get_kiwix_summary(search_term):
"""Query local Kiwix server for Wikipedia article"""
try:
search_encoded = quote(search_term)
# Try direct article access first
wiki_article = search_encoded.capitalize().replace("%20", "_")
exact_url = f"{kiwix_url}/raw/{kiwix_library_name}/content/A/{wiki_article}"
response = requests.get(exact_url, timeout=urlTimeoutSeconds)
if response.status_code == 200:
# Extract and clean text
text = text_from_html(response.text)
# Remove common Wikipedia metadata prefixes
text = text.split("Jump to navigation", 1)[-1]
text = text.split("Jump to search", 1)[-1]
# Truncate to reasonable length (first few sentences)
sentences = text.split('. ')
summary = '. '.join(sentences[:wiki_return_limit])
if summary and not summary.endswith('.'):
summary += '.'
return summary.strip()[:500] # Hard limit at 500 chars
# If direct access fails, try search
search_url = f"{kiwix_url}/search?content={kiwix_library_name}&pattern={search_encoded}"
response = requests.get(search_url, timeout=urlTimeoutSeconds)
if response.status_code == 200 and "No results were found" not in response.text:
soup = BeautifulSoup(response.text, 'html.parser')
links = [a['href'] for a in soup.find_all('a', href=True) if "start=" not in a['href']]
for link in links[:3]: # Check first 3 results
article_name = link.split("/")[-1]
if not article_name or article_name[0].islower():
continue
article_url = f"{kiwix_url}{link}"
article_response = requests.get(article_url, timeout=urlTimeoutSeconds)
if article_response.status_code == 200:
text = text_from_html(article_response.text)
text = text.split("Jump to navigation", 1)[-1]
text = text.split("Jump to search", 1)[-1]
sentences = text.split('. ')
summary = '. '.join(sentences[:wiki_return_limit])
if summary and not summary.endswith('.'):
summary += '.'
return summary.strip()[:500]
logger.warning(f"System: No Kiwix Results for:{search_term}")
# try to fall back to online Wikipedia if available
return get_wikipedia_summary(search_term, force=True)
except requests.RequestException as e:
logger.warning(f"System: Kiwix connection error: {e}")
return "Unable to connect to local wiki server"
except Exception as e:
logger.warning(f"System: Error with Kiwix for:{search_term} {e}")
return ERROR_FETCHING_DATA
def get_wikipedia_summary(search_term, location=None, force=False):
lat, lon = location if location else (None, None)
# Use Kiwix if configured
if use_kiwix_server and not force:
return get_kiwix_summary(search_term)
try:
# Otherwise use online Wikipedia
wikipedia_search = wikipedia.search(search_term, results=3)
wikipedia_suggest = wikipedia.suggest(search_term)
#wikipedia_aroundme = wikipedia.geosearch(lat,lon, results=3)
#logger.debug(f"System: Wikipedia Nearby:{wikipedia_aroundme}")
except Exception as e:
logger.debug(f"System: Wikipedia search error for:{search_term} {e}")
return ERROR_FETCHING_DATA
if len(wikipedia_search) == 0:
logger.warning(f"System: No Wikipedia Results for:{search_term}")
return ERROR_FETCHING_DATA
try:
logger.debug(f"System: Searching Wikipedia for:{search_term}, First Result:{wikipedia_search[0]}, Suggest Word:{wikipedia_suggest}")
summary = wikipedia.summary(search_term, sentences=wiki_return_limit, auto_suggest=False, redirect=True)
except wikipedia.DisambiguationError as e:
logger.warning(f"System: Disambiguation Error for:{search_term} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except wikipedia.PageError as e:
logger.warning(f"System: Wikipedia Page Error for:{search_term} {e} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except Exception as e:
logger.warning(f"System: Error with Wikipedia for:{search_term} {e}")
return ERROR_FETCHING_DATA
return summary

View File

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

View File

@@ -2,30 +2,42 @@
# Meshtastic Autoresponder PONG Bot
# K7MHI Kelly Keeton 2024
try:
from pubsub import pub
except ImportError:
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh pong' using a virtual environment.")
exit(1)
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
import random
from modules.log import *
from modules.system import *
# Global Variables
DEBUGpacket = False # Debug print the packet rx
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
# Auto response to messages
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓Ping!!",
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"echo": lambda: handle_echo(message, message_from_id, deviceID, isDM, channel_number),
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"motd": lambda: handle_motd(message, MOTD),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(hop, snr, rssi),
"test": lambda: handle_testing(hop, snr, rssi),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
}
cmds = [] # list to hold the commands found in the message
for key in command_handler:
@@ -44,85 +56,175 @@ 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]
else:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🏓PONG, " + hop
def handle_cmd(message, message_from_id, deviceID):
# why CMD? its just a command list. a terminal would normally use "Help"
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
if " " in message and message.split(" ")[1] in trap_list:
return "🤖 just use the commands directly in chat"
return help_message
def handle_motd(message):
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
msg = ""
type = ''
if "ping" in message.lower():
msg = "🏓PONG\n"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
"🎙Testing, testing\n",\
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
"🎙Roger that!\n",])
type = "🎙TEST"
elif "ack" in message.lower():
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
msg = f"QSP QSL OM DE {myname} K\n"
else:
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
else:
msg = msg + hop
if "@" in message:
msg = msg + " @" + message.split("@")[1]
type = type + " @" + message.split("@")[1]
elif "#" in message:
msg = msg + " #" + message.split("#")[1]
type = type + " #" + message.split("#")[1]
# check for multi ping request
if " " in message:
# if stop multi ping
if "stop" in message.lower():
for i in range(0, len(multiPingList)):
if multiPingList[i].get('message_from_id') == message_from_id:
multiPingList.pop(i)
msg = "🛑 auto-ping"
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
def handle_motd(message, message_from_id, isDM):
global MOTD
if "$" in message:
isAdmin = False
msg = ""
# check if the message_from_id is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
if str(message_from_id) == admin:
isAdmin = True
break
else:
isAdmin = True
# admin help via DM
if "?" in message and isDM and isAdmin:
msg = "Message of the day, set with 'motd $ HelloWorld!'"
elif "?" in message and isDM and not isAdmin:
# non-admin help via DM
msg = "Message of the day"
elif "$" in message and isAdmin:
motd = message.split("$")[1]
MOTD = motd.rstrip()
return "MOTD Set to: " + MOTD
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
msg = "MOTD changed to: " + MOTD
else:
return MOTD
msg = "MOTD: " + MOTD
return msg
def handle_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_echo(message, message_from_id, deviceID, isDM, channel_number):
if "?" in message.lower():
return "echo command returns your message back to you. Example:echo Hello World"
elif "echo " in message.lower():
parts = message.lower().split("echo ", 1)
if len(parts) > 1 and parts[1].strip() != "":
echo_msg = parts[1]
if channel_number != echoChannel:
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
return echo_msg
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
def sysinfo(message, message_from_id, deviceID):
if "?" in message:
return "sysinfo command returns system information."
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
# display last heard nodes add to response
bot_response = "Last Heard\n"
bot_response += str(get_node_list(1))
# bot_response += getNodeTelemetry(deviceID)
return bot_response
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "✋ACK-ACK! " + hop
def handle_testing(hop, snr, rssi):
if hop == "Direct":
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🎙Testing 1,2,3 " + hop
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxInterface}")
if port1 in rxInterface:
retry_int1 = True
elif interface2_enabled and port2 in rxInterface:
retry_int2 = True
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxHost}")
if hostname1 in rxHost and interface1_type == 'tcp':
retry_int1 = True
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
retry_int2 = True
if rxType == 'BLEInterface':
logger.critical(f"System: Lost Connection to Device BLE")
if interface1_type == 'ble':
retry_int1 = True
elif interface2_enabled and interface2_type == 'ble':
retry_int2 = True
def onReceive(packet, interface):
# extract interface defailts from interface object
global seenNodes
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
# extract interface details from inbound packet
rxType = type(interface).__name__
rxNode = 0
message_from_id = 0
snr = 0
rssi = 0
hop = 0
hop_away = 0
# Valies assinged to the packet
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
replyIDset = False
emojiSeen = False
simulator_flag = False
isDM = False
channel_name = "unknown"
playingGame = False
if DEBUGpacket:
# Debug print the interface object
for item in interface.__dict__.items(): intDebug = f"{item}\n"
@@ -130,112 +232,191 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface:
rxNode = 1
elif interface2_enabled and port2 in rxInterface:
rxNode = 2
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp':
rxNode = 1
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
if packet.get('channel'):
channel_number = packet.get('channel')
# get channel name from channel number from connected devices
for device in channel_list:
if device["interface_id"] == rxNode:
device_channels = device['channels']
for chan_name, info in device_channels.items():
if info['number'] == channel_number:
channel_name = chan_name
break
# check for a message packet and process it
# get channel hashes for the interface
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
if device:
# Find the channel name whose hash matches channel_number
for chan_name, info in device['channels'].items():
if info['hash'] == channel_number:
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
channel_name = chan_name
break
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
try:
snr = packet['rxSnr']
rssi = packet['rxRssi']
except KeyError:
snr = 0
rssi = 0
via_mqtt = packet['decoded'].get('viaMqtt', False)
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has 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)
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet['hopsAway']
else:
# if the packet does not have a hop count try other methods
hop_away = 0
if packet.get('hopLimit'):
hop_limit = packet['hopLimit']
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet['hopStart']
else:
hop_start = 0
hop_away = packet.get('hopsAway', 0)
if hop_start == hop_limit:
hop = "Direct"
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
hop_count = hop_away
else:
hop_count = hop_start - hop_limit
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
hop = f"{hop_count} hops"
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# calculate hop count
hop = ""
if hop_limit > 0 and hop_start >= hop_limit:
hop_count = hop_away + (hop_start - hop_limit)
elif hop_limit > 0 and hop_start < hop_limit:
hop_count = hop_away + (hop_limit - hop_start)
else:
hop_count = hop_away
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
hop = "Direct"
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
hop = "IP-Network"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
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 stdout
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
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}")
# log the message to the message log
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
if ignoreDefaultChannel and channel_number == publicChannel:
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
else:
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history list
@@ -257,45 +438,66 @@ def onReceive(packet, interface):
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
if repeater_enabled and multiple_interface:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
send_message(rMsg, channel_number, 0, i)
time.sleep(responseDelay)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode, channel_number)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False):
myNodeNum = globals().get(f'myNodeNum{i}', 0)
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
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 motd_enabled:
logger.debug(f"System: MOTD Enabled using {MOTD}")
if enableEcho:
logger.debug(f"System: Echo command Enabled")
if sentry_enabled:
logger.debug("System: Sentry Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug("System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if highfly_enabled:
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
if repeater_enabled and multiple_interface:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if 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 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:
@@ -304,16 +506,49 @@ async def start_rx():
# Hello World
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
await asyncio.wait([meshRxTask, watchdogTask])
tasks = []
try:
# Create core tasks
tasks.append(asyncio.create_task(start_rx(), name="pong_rx"))
tasks.append(asyncio.create_task(watchdog(), name="watchdog"))
# Add optional tasks
if file_monitor_enabled:
tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor"))
logger.debug(f"System: Starting {len(tasks)} async tasks")
# Wait for all tasks with proper exception handling
results = await asyncio.gather(*tasks, return_exceptions=True)
# Check for exceptions in results
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.error(f"Task {tasks[i].get_name()} failed with: {result}")
except Exception as e:
logger.error(f"Main loop error: {e}")
finally:
# Cleanup tasks
logger.debug("System: Cleaning up async tasks")
for task in tasks:
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
logger.debug(f"Task {task.get_name()} cancelled successfully")
except Exception as e:
logger.warning(f"Error cancelling task {task.get_name()}: {e}")
try:
asyncLoop = asyncio.new_event_loop()
if __name__ == "__main__":
await asyncio.sleep(0.01)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
exit_handler()
pass
except KeyboardInterrupt:
exit_handler()
except SystemExit:
pass
# EOF

View File

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

130
script/addFav.py Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# Add a favorite node to all interfaces from config.ini data
# meshing-around - helper script
import sys
import os
import pickle
import argparse
favList = []
roofNodeList = []
roof_node = False
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Add favorite nodes or print pickle contents.")
parser.add_argument('-pickle', '-p', action='store_true', help="Print the contents of roofNodeList.pkl and exit")
args = parser.parse_args()
if args.pickle:
try:
with open('roofNodeList.pkl', 'rb') as f:
data = pickle.load(f)
#print a simple list of nodeID:x\n
for item in data:
print(f"{item.get('nodeID', 'N/A')}")
except Exception as e:
print(f"Error reading roofNodeList.pkl: {e}")
exit(0)
# welcome header
print("meshing-around: addFav - Auto-Add favorite nodes to all interfaces from config.ini data")
print("This script may need API improvments still in progress")
print("---------------------------------------------------------------")
try:
# set the path to import the modules and config.ini
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from modules.log import *
from modules.system import *
except Exception as e:
print(f"Error importing modules run this program from the main repo directory 'python3 script/addFav.py'")
print(f"if you forgot the rest of it.. git clone https://github.com/spudgunman/meshing-around")
print(f"Import Error: {e}")
exit(1)
try:
# ask if we are running on a roof node
print("This script can be run on a client_base or on the bot under a roof node.")
print("The purpose of this script is to add favorite nodes to the bot to retain DM keys.")
print("If you are running this script on a roof (base) node, stop and rerun it on the bot first to collect all node ID's.")
roof_node = input("Are you running this script on a client_base node which has no BOT? (y/n): ").strip().lower()
if roof_node not in ['y', 'n']:
raise ValueError("Invalid input. Please enter 'y' or 'n'.")
roof_node = (roof_node == 'y')
except Exception as e:
print(f"Error: {e}")
exit(1)
try:
if roof_node:
# load roofNodeList from pickle file
try:
with open('roofNodeList.pkl', 'rb') as f:
roofNodeList = pickle.load(f)
logger.info(f"addFav: Loaded {len(roofNodeList)} connected nodes from roofNodeList.pkl for use on roof client_base only")
print(f"Loaded {len(roofNodeList)} connected nodes from roofNodeList.pkl for use on roof client_base only")
except Exception as e:
logger.error(f"addFav: Error loading roofNodeList.pkl: {e} - run this program from the main program directory 'python3 script/addFav.py'")
exit(1)
favList = roofNodeList
else:
# compile the favorite list wich returns node,interface tuples
roofNodeList = compileFavoriteList(True)
favList = compileFavoriteList(False)
#combine favList and roofNodeList to save for next step
for node in roofNodeList:
if node not in favList:
favList.append(node)
#save roofNodeList to a pickle file for running on the roof node
with open('roofNodeList.pkl', 'wb') as f:
pickle.dump(roofNodeList, f)
logger.info(f"addFav: Saved {len(roofNodeList)} connected nodes to roofNodeList.pkl for use on roof client_base only")
print(f"Saved {len(roofNodeList)} connected nodes to roofNodeList.pkl for use on roof client_base only")
except Exception as e:
logger.error(f"addFav: Error compiling favorite list: {e} - run this program from the main program directory 'python3 script/addFav.py'")
exit(1)
#confirm you want all these added
try:
if favList:
print(f"The following {len(favList)} favorite nodes will be added to the device(s):")
count_devices = set([fav['deviceID'] for fav in favList])
count_nodes = set([fav['nodeID'] for fav in favList])
for fav in favList:
print(f"addFav: adding nodeID {fav['nodeID']} meshtastic --set-favorite-node {fav['nodeID']}")
confirm = input(f"Are you sure you want to add these {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)? (y/n): ").strip().lower()
if confirm != 'y':
print("Operation cancelled by user.")
exit(0)
else:
print("No favorite nodes to add to device(s). Exiting.")
exit(0)
except Exception as e:
logger.error(f"addFav: Error during confirmation: {e}")
exit(1)
if favList:
# for each node,interface tuple add the favorite node
for fav in favList:
try:
handleFavoriteNode(fav['deviceID'], fav['nodeID'], True)
logger.info(f"addFav: waiting 15 seconds to avoid API rate limits")
time.sleep(15) # wait to avoid API rate limits
except Exception as e:
logger.error(f"addFav: Error adding favorite node {fav['nodeID']} to device {fav['deviceID']}: {e}")
else:
logger.info("addFav: No favorite nodes to add to device(s)")
exit(0)
count_devices = set([fav['deviceID'] for fav in favList])
count_nodes = set([fav['nodeID'] for fav in favList])
logger.info(f"addFav: Finished adding {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)")
logger.info("addFav: You may need to restart the mesh service on the device(s)")
print(f"Finished adding {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)")
print(f"Data file for roof client_base has been saved to roofNodeList.pkl")
if not roof_node:
logger.info(f"addFav: You can now run this repo+script & roofNodeList.pkl on the roof node to add the favorite nodes to the roof client_base")
exit(0)

104
script/configMerge.py Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Configuration Merge Script
# Merges user configuration with default settings
# 2025 Kelly Keeton K7MHI mesh-around and its meshtastic
import shutil
import configparser
import os
def merge_configs(default_config_path, user_config_path, output_config_path):
# Load default configuration (INI)
default_config = configparser.ConfigParser()
default_config.read(default_config_path)
# Load user configuration (INI)
user_config = configparser.ConfigParser()
user_config.read(user_config_path)
# Merge configurations
for section in user_config.sections():
if not default_config.has_section(section):
default_config.add_section(section)
for key, value in user_config.items(section):
default_config.set(section, key, value)
# Save merged configuration as INI
with open(output_config_path, 'w', encoding='utf-8') as f:
default_config.write(f)
def backup_config(config_path, backup_path):
shutil.copyfile(config_path, backup_path)
def show_config_changes(user_config_path, merged_config_path):
if not os.path.exists(merged_config_path) or os.path.getsize(merged_config_path) == 0:
print(f"Error: {merged_config_path} is empty or missing!")
return
# Load user config (as dict)
user_config = configparser.ConfigParser()
user_config.read(user_config_path)
user_dict = {s: dict(user_config.items(s)) for s in user_config.sections()}
# Load merged config (as dict)
merged_config = configparser.ConfigParser()
merged_config.read(merged_config_path)
merged_dict = {s: dict(merged_config.items(s)) for s in merged_config.sections()}
print("\n--- Changes in merged configuration ---")
for section in merged_dict:
if section not in user_dict:
print(f"[{section}] (new section)")
for k, v in merged_dict[section].items():
print(f" {k} = {v} (added)")
else:
for k, v in merged_dict[section].items():
if k not in user_dict[section]:
print(f"[{section}] {k} = {v} (added)")
elif user_dict[section][k] != v:
print(f"[{section}] {k}: {user_dict[section][k]} -> {v} (changed)")
print("--- End of changes ---\n")
if __name__ == "__main__":
print("MESHING-AROUND: Configuration Merge Script for config.ini checking updates from config.template")
print("---------------------------------------------------------------")
master_config_path = 'config.template'
user_config_path = 'config.ini'
output_config = 'config_new.ini'
backup_config_path = 'config.bak'
# Step 1: Check master config
try:
if not os.path.exists(master_config_path) or os.path.getsize(master_config_path) == 0:
raise FileNotFoundError(f"Master configuration file {master_config_path} is missing or empty.")
except Exception as e:
print(f"Error: {e}")
print("Run the tool from the meshing-around/script/ directory where the config.template is located.")
print(" python3 script/configMerge.py")
exit(1)
# Step 2: Backup user config
try:
backup_config(user_config_path, backup_config_path)
print(f"Backup of user config created at {backup_config_path}")
except Exception as e:
print(f"Error backing up user config: {e}")
exit(1)
# Step 3: Merge configs
try:
merge_configs(master_config_path, user_config_path, output_config)
print(f"Merged configuration saved to {output_config}")
except Exception as e:
print(f"Error merging configuration: {e}")
exit(1)
# Step 4: Show changes
try:
show_config_changes(user_config_path, output_config)
print("Please review the new configuration and replace your existing config.ini if needed.")
print(" cp config_new.ini config.ini")
except Exception as e:
print(f"Error showing configuration changes: {e}")
exit(1)

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

@@ -0,0 +1,25 @@
# How do I use this thing?
This is not a full turnkey setup for Docker yet but gets you most of the way there!
## Setup New Image
`docker build -t meshing-around .`
there is also [script/docker/docker-install.bat](script/docker/docker-install.bat) which will automate this.
## Ollama Image with compose
still a WIP
`docker compose up -d`
## Edit the config.ini in the docker
To edit the config.ini in the docker you can
`docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"`
there is also [script/docker/docker-terminal.bat](script/docker/docker-terminal.bat) which will open nano to edit.
ctl+o to write out and exit editor in shell
## other info
1. Ensure your serial port is properly shared.
2. Run the Docker container:
```sh
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
```

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
REM launch meshing-around container with a terminal
docker run -it --entrypoint /bin/bash meshing-around

View File

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

View File

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

53
script/injectDM.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
# Usage: python3 script/injectDM.py -s NODEID -d NODEID -m "message"
# meshing-around - helper script
import sys
import os
import argparse
# welcome header
print("meshing-around: injectDM.py -s NODEID -d NODEID -m 'Hello World'")
print("Auto-Inject DM messages to data/bbsdm.pkl")
print(" needs config.ini [bbs] bbsAPI_enabled = True ")
print("---------------------------------------------------------------")
try:
# set the path to import the modules and config.ini
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from modules.log import *
from modules.bbstools import *
except Exception as e:
print(f"Error importing modules run this program from the main program directory 'python3 script/injectDM.py'")
exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Inject DM messages to data/bbsdm.pkl')
parser.add_argument('-s', '--src', type=str, required=True, help='Source NODEID')
parser.add_argument('-d', '--dst', type=str, required=True, help='Destination NODEID')
parser.add_argument('-m', '--msg', type=str, required=True, help="'Message to send'")
args = parser.parse_args()
dst = args.dst
src = args.src
message = args.msg
if not message:
logger.error("Message cannot be empty")
exit(1)
if dst == src:
logger.error("Source and Destination cannot be the same")
exit(1)
if not isinstance(bbs_dm, list):
logger.error("bbs_dm is corrupt, something is wrong")
exit(1)
# inject the message
if bbs_post_dm(dst, message, src):
logger.info(f"Injected message from {src} to {dst}: {message}")
else:
logger.error("Failed to inject message")
exit(1)
# show stats get_bbs_stats
stats = get_bbs_stats()
stats = stats.replace("\n", " | ")
logger.info(f"BBS Stats: {stats}")

7
script/runShell.sh Normal file
View File

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

44
script/sysEnv.sh Normal file
View File

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

87
update.sh Normal file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
# MeshBot Update Script
# Usage: bash update.sh or ./update.sh after making it executable with chmod +x update.sh
# Check if the mesh_bot.service or pong_bot.service
if systemctl is-active --quiet mesh_bot.service; then
echo "Stopping mesh_bot.service..."
systemctl stop mesh_bot.service
service_stopped=true
fi
if systemctl is-active --quiet pong_bot.service; then
echo "Stopping pong_bot.service..."
systemctl stop pong_bot.service
service_stopped=true
fi
if systemctl is-active --quiet mesh_bot_reporting.service; then
echo "Stopping mesh_bot_reporting.service..."
systemctl stop mesh_bot_reporting.service
service_stopped=true
fi
if systemctl is-active --quiet mesh_bot_w3.service; then
echo "Stopping mesh_bot_w3.service..."
systemctl stop mesh_bot_w3.service
service_stopped=true
fi
# Fetch latest changes from GitHub
echo "Fetching latest changes from GitHub..."
if ! git fetch origin; then
echo "Error: Failed to fetch from GitHub, check your network connection."
exit 1
fi
# git pull with rebase to avoid unnecessary merge commits
echo "Pulling latest changes from GitHub..."
if ! git pull origin main --rebase; then
read -p "Git pull resulted in conflicts. Do you want to reset hard to origin/main? This will discard local changes. (y/n): " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
git fetch --all
git reset --hard origin/main
echo "Local repository updated."
else
echo "Update aborted due to git conflicts."
fi
fi
# Backup the data/ directory
echo "Backing up data/ directory..."
#backup_file="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
backup_file="data_backup.tar.gz"
path2backup="data/"
tar -czf "$backup_file" "$path2backup"
if [ $? -ne 0 ]; then
echo "Error: Backup failed."
else
echo "Backup of ${path2backup} completed: ${backup_file}"
fi
# Build a config_new.ini file merging user config with new defaults
echo "Merging configuration files..."
python3 script/configMerge.py > ini_merge_log.txt 2>&1
if [ -f ini_merge_log.txt ]; then
if grep -q "Error during configuration merge" ini_merge_log.txt; then
echo "Configuration merge encountered errors. Please check ini_merge_log.txt for details."
else
echo "Configuration merge completed. Please review config_new.ini and ini_merge_log.txt."
fi
else
echo "Configuration merge log (ini_merge_log.txt) not found. check out the script/configMerge.py tool!"
fi
# if service was stopped earlier, restart it
if [ "$service_stopped" = true ]; then
echo "Restarting services..."
systemctl start mesh_bot.service
systemctl start pong_bot.service
systemctl start mesh_bot_reporting.service
systemctl start mesh_bot_w3.service
echo "Services restarted."
fi
# Print completion message
echo "Update completed successfully?"
exit 0
# End of script