Compare commits

...

309 Commits

Author SHA1 Message Date
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
38 changed files with 3969 additions and 1376 deletions

7
.gitignore vendored
View File

@@ -23,7 +23,8 @@ data/rag/*
# qrz db
data/qrz.db
# fileMon
news.txt
alert.txt
# fileMonitor test file
bee.txt
# .csv files
*.csv

View File

@@ -41,6 +41,15 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### 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.
@@ -49,10 +58,21 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **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.
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
@@ -63,11 +83,12 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### File Monitor Alerts
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
- **News File**: On request of news, the contents of the file are returned.
- **Shell Command Access**: Pass commands via DM directly to the host OS
- **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.
@@ -92,14 +113,16 @@ git clone https://github.com/spudgunman/meshing-around
| `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 220) via DM only | ✅ |
| `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 | ✅ |
### Radio Propagation & Weather Forecasting
| Command | Description | |
@@ -108,7 +131,7 @@ git clone https://github.com/spudgunman/meshing-around
| `earthquake` | Returns the largest and number of USGS events for the location | |
| `hfcond` | Returns a table of HF solar conditions | |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
| `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) | |
@@ -138,9 +161,10 @@ git clone https://github.com/spudgunman/meshing-around
|---------|-------------|-
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard on device, like Store and Forward, returns the PublicChannel and Current | ✅ |
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
| `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 and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `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 | ✅ |
@@ -162,8 +186,19 @@ git clone https://github.com/spudgunman/meshing-around
| `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
@@ -194,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]
@@ -229,6 +264,11 @@ The weather forecasting defaults to NOAA, for locations outside the USA, you can
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
@@ -377,6 +417,33 @@ googleSearchResults = 3 # number of google search results to include in the cont
```
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.**
@@ -431,7 +498,12 @@ rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas
```
#### Newspaper on mesh
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
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
@@ -454,7 +526,7 @@ value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
interval = # interval to use when time is not set (e.g. every 2 days)
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
```
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
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
@@ -465,7 +537,7 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
```
#### BBS Link
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 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))
@@ -523,7 +595,8 @@ I used ideas and snippets from other responder bots and want to call them out!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Iris**: testing and finding 🐞
- **Cisien, bitflip, Woof, propstg, trs2982, Josh, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **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

View File

@@ -1,7 +1,7 @@
#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]
@@ -55,8 +55,23 @@ 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
@@ -73,6 +88,7 @@ rawLLMQuery = True
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
reverseSF = False
# history command
enableCmdHistory = True
@@ -82,7 +98,7 @@ 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
@@ -111,6 +127,7 @@ 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
@@ -132,6 +149,7 @@ highFlyingAlertAltitude = 2000
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 =
@@ -154,6 +172,8 @@ bbsAPI_enabled = False
enabled = True
lat = 48.50
lon = -123.0
fuzzConfigLocation = True
fuzzItAll = False
# Default to metric units rather than imperial
useMetric = False
@@ -179,7 +199,8 @@ myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz1
# number of data points to return, default is 3
coastalForecastDays = 3
# NOAA USGS Hydrology river identifiers, LID or USGS ID https://waterdata.usgs.gov
# 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
@@ -254,7 +275,9 @@ interface = 1
# channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
# 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. or custom for module/scheduler.py
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
@@ -265,7 +288,9 @@ time =
# 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
@@ -274,6 +299,17 @@ 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]
filemon_enabled = False
@@ -284,7 +320,7 @@ broadcastCh = 2
# news command will return the contents of a text file
enable_read_news = False
news_file_path = news.txt
news_file_path = ../data/news.txt
# only return a single random line from the news file
news_random_line = False
@@ -293,6 +329,10 @@ 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
@@ -335,22 +375,35 @@ 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 for sending at high success rate, chunkr allows exceeding by 3 characters
# 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
# 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

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.

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."
}
]

224
etc/meshview.ino Normal file
View File

@@ -0,0 +1,224 @@
// Example to receive and decode Meshtastic UDP packets
// Make sure to install the meashtastic library and generate the .pb.h and .pb.c files from the Meshtastic .proto definitions
// https://github.com/meshtastic/protobufs/tree/master/meshtastic
// Example to receive and decode Meshtastic UDP packets
#include <WiFi.h>
#include <WiFiUdp.h>
// #include <AESLib.h> // or another AES library
#include "pb_decode.h"
#include "meshtastic/mesh.pb.h" // MeshPacket, Position, etc.
#include "meshtastic/portnums.pb.h" // Port numbers enum
#include "meshtastic/telemetry.pb.h" // Telemetry message
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* default_key = "1PG7OiApB1nwvP+rz05pAQ=="; // Your network key here
uint8_t aes_key[16]; // Buffer for decoded key
const char* MCAST_GRP = "224.0.0.69";
const uint16_t MCAST_PORT = 4403;
unsigned long udpPacketCount = 0;
WiFiUDP udp;
IPAddress multicastIP;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Scanning for WiFi networks...");
int n = WiFi.scanNetworks();
if (n == 0) {
Serial.println("No networks found.");
} else {
Serial.print(n);
Serial.println(" networks found:");
for (int i = 0; i < n; ++i) {
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (RSSI ");
Serial.print(WiFi.RSSI(i));
Serial.print(")");
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " [OPEN]" : " [SECURED]");
delay(10);
}
}
Serial.println("Connecting to WiFi...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long startAttemptTime = millis();
const unsigned long wifiTimeout = 20000;
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiTimeout) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected.");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
multicastIP.fromString(MCAST_GRP);
if (udp.beginMulticast(multicastIP, MCAST_PORT)) {
Serial.println("UDP multicast listener started.");
} else {
Serial.println("Failed to start UDP multicast listener.");
}
} else {
Serial.print("\nFailed to connect to WiFi. SSID: ");
Serial.println(ssid);
Serial.println("Check SSID, range, and password.");
}
}
void printHex(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++) {
Serial.printf("%02X ", buf[i]);
}
Serial.println();
}
void printAscii(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++) {
char c = static_cast<char>(buf[i]);
Serial.print(isprint(c) ? c : '.');
}
Serial.println();
}
void decodeKey() {
// Convert base64 key to raw bytes
// You may need to add a base64 decoding function/library
// Example: decode_base64(default_key, aes_key, sizeof(aes_key));
}
void decryptPayload(const uint8_t* encrypted, size_t len, uint8_t* decrypted) {
// Use AESLib or similar to decrypt
// Example: aes128_dec_single(decrypted, encrypted, aes_key);
}
void loop() {
int packetSize = udp.parsePacket();
if (!packetSize) {
delay(50);
return;
}
udpPacketCount++;
Serial.print("UDP packets seen: ");
Serial.println(udpPacketCount);
uint8_t buffer[512];
int len = udp.read(buffer, sizeof(buffer));
if (len <= 0) {
Serial.println("Failed to read UDP packet.");
delay(50);
return;
}
// Always show raw payload
Serial.print("Raw UDP payload (hex): ");
printHex(buffer, len);
Serial.print("Raw UDP payload (ASCII): ");
printAscii(buffer, len);
// Decode outer MeshPacket
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_zero;
pb_istream_t stream = pb_istream_from_buffer(buffer, len);
if (!pb_decode(&stream, meshtastic_MeshPacket_fields, &pkt)) {
Serial.println("Failed to decode meshtastic_MeshPacket.");
delay(50);
return;
}
// Basic MeshPacket fields
Serial.print("id: "); Serial.println(pkt.id);
Serial.print("rx_time: "); Serial.println(pkt.rx_time);
Serial.print("rx_snr: "); Serial.println(pkt.rx_snr, 2);
Serial.print("rx_rssi: "); Serial.println(pkt.rx_rssi);
Serial.print("hop_limit: "); Serial.println(pkt.hop_limit);
Serial.print("priority: "); Serial.println(pkt.priority);
Serial.print("from: "); Serial.println(pkt.from);
Serial.print("to: "); Serial.println(pkt.to);
Serial.print("channel: "); Serial.println(pkt.channel);
// Only proceed if we have a decoded Data variant
if (pkt.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
Serial.println("Packet does not contain decoded Data (maybe encrypted or other variant).");
delay(50);
return;
}
const meshtastic_Data& data = pkt.decoded;
Serial.print("Portnum: "); Serial.println(data.portnum);
Serial.print("Payload size: "); Serial.println(data.payload.size);
if (data.payload.size == 0) {
Serial.println("No inner payload bytes.");
delay(50);
return;
}
// Decode by portnum
switch (data.portnum) {
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
// Current schemas do not use a separate user.pb.h. Text payload is plain bytes.
Serial.print("Decoded text message: ");
printAscii(data.payload.bytes, data.payload.size);
break;
}
case meshtastic_PortNum_POSITION_APP: {
meshtastic_Position pos = meshtastic_Position_init_zero;
pb_istream_t ps = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
if (pb_decode(&ps, meshtastic_Position_fields, &pos)) {
Serial.print("Position lat="); Serial.print(pos.latitude_i / 1e7, 7);
Serial.print(" lon="); Serial.print(pos.longitude_i / 1e7, 7);
Serial.print(" alt="); Serial.println(pos.altitude);
} else {
Serial.println("Failed to decode Position payload.");
}
break;
}
case meshtastic_PortNum_TELEMETRY_APP: {
meshtastic_Telemetry tel = meshtastic_Telemetry_init_zero;
pb_istream_t ts = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
if (pb_decode(&ts, meshtastic_Telemetry_fields, &tel)) {
// Print a few common fields if present
if (tel.which_variant == meshtastic_Telemetry_device_metrics_tag) {
const meshtastic_DeviceMetrics& m = tel.variant.device_metrics;
Serial.print("Telemetry battery_level="); Serial.print(m.battery_level);
Serial.print(" voltage="); Serial.print(m.voltage);
Serial.print(" air_util_tx="); Serial.println(m.air_util_tx);
} else {
Serial.println("Telemetry decoded, different variant. Raw bytes:");
printHex(data.payload.bytes, data.payload.size);
}
} else {
Serial.println("Failed to decode Telemetry payload.");
}
break;
}
default: {
Serial.print("Unhandled portnum "); Serial.print((int)data.portnum);
Serial.println(", showing payload as hex:");
printHex(data.payload.bytes, data.payload.size);
break;
}
}
delay(50);
}

View File

@@ -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

@@ -287,7 +287,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
# document the service install
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
printf "sudo 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

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,19 @@ 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")
# global message list, later we will use a pickle on disk
bbs_messages = []
bbs_dm = []
def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
@@ -88,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 = today.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
@@ -96,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
@@ -153,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)])
@@ -187,6 +208,32 @@ 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
@@ -231,7 +278,13 @@ def bbs_sync_posts(input, peerNode, RxNode):
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
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))

View File

@@ -9,11 +9,12 @@ import subprocess
trap_list_filemon = ("readnews",)
def read_file(file_monitor_file_path, random_line_only=False):
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):
logger.warning(f"FileMon: File not found: {file_monitor_file_path}")
if file_monitor_file_path == "bee.txt":
return "🐝buzz 💐buzz buzz🍯"
if random_line_only:
@@ -29,21 +30,25 @@ def read_file(file_monitor_file_path, random_line_only=False):
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
def read_news():
# read the news file on demand
return read_file(news_file_path, news_random_line_only)
def 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:
with open(news_file_path, 'a' if append else 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"FileMon: Updated {news_file_path}")
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: {news_file_path}")
logger.warning(f"FileMon: Error writing file: {file_path}")
return False
async def watch_file():
@@ -67,7 +72,6 @@ async def watch_file():
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
try:
# Debugging: Print the current working directory and resolved script path
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
@@ -77,45 +81,97 @@ def call_external_script(message, script="script/runShell.sh"):
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
# 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"
if enable_runShellCmd:
# clean up the command input
# 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:]
if command.startswith(" "):
command = command[1:]
command = command.strip()
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 "x: invalid command format"
# Run the shell command as a subprocess
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()
if output:
return output
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 "x: command is disabled"
return "x: command executed with no output"
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 message.lower() == "blackjack":
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 = {}
@@ -366,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
@@ -375,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)})
@@ -680,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

View File

@@ -55,6 +55,8 @@ lameJokes = [
"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': '😎',
@@ -166,10 +168,10 @@ def sendWithEmoji(message):
i += 1
return ' '.join(words)
def tell_joke(nodeID=0):
def tell_joke(nodeID=0, vox=False):
dadjoke = Dadjoke()
try:
if dad_jokes_emojiJokes:
if dad_jokes_emojiJokes or vox:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke

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()

View File

@@ -3,6 +3,8 @@
# 2025
from modules.log import *
import random
import time
# to molly and jake, I miss you both so much.
if disable_emojis_in_games:
@@ -29,7 +31,7 @@ class TicTacToe:
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
if games > 0:
if games > 3:
if won / games >= 3.14159265358979323846: # win rate > pi
ret += random.choice(positiveThoughts) + "\n"
else:
@@ -47,6 +49,10 @@ class TicTacToe:
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"""
@@ -90,19 +96,30 @@ class TicTacToe:
return True
def bot_move(self, id):
"""AI makes a move"""
"""AI makes a move: tries to win, block, or pick random"""
g = self.game[id]
# Simple AI: Try to win, block, or pick random
move = self.find_winning_move(id, O) # Try to win
if move == -1:
move = self.find_winning_move(id, X) # Block player
if move == -1:
move = self.find_random_move(id) # Random move
board = g["board"]
# Try to win
move = self.find_winning_move(id, O)
if move != -1:
g["board"][move] = O
return move
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"""
@@ -117,12 +134,22 @@ class TicTacToe:
return i
board[i] = " "
return -1
def find_random_move(self, id):
"""Find a random empty position"""
g = self.game[id]
empty = [i for i in range(9) if g["board"][i] == " "]
return random.choice(empty) if empty else -1
def 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"""
@@ -156,7 +183,7 @@ class TicTacToe:
if winner == X:
g["won"] += 1
return "🎉You won! (n)ew (e)nd"
elif winner == X:
elif winner == O:
return "🤖Bot wins! (n)ew (e)nd"
else:
return "🤝Tie, The only winning move! (n)ew (e)nd"

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:
@@ -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":
@@ -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!"

View File

@@ -48,7 +48,7 @@ meshBotAI = """
PROMPT
{input}
"""
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
@@ -76,6 +76,142 @@ if llmEnableHistory:
"""
# 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:
# 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:
return f"Error in calculation: {e}"
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:
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:
return [f"Error in Google search: {e}"]
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:
googleSearch = search(input, advanced=True, num_results=num_results)
if googleSearch:
for result in googleSearch:
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']
return googleResults
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 = []
@@ -85,6 +221,10 @@ def llm_query(input, nodeID=0, location_name=None):
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 "
@@ -105,23 +245,7 @@ def llm_query(input, nodeID=0, location_name=None):
antiFloodLLM.append(nodeID)
if llmContext_fromGoogle and not rawLLMQuery:
# grab some context from the internet using google search hits (if available)
# localization details at https://pypi.org/project/googlesearch-python/
# 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']
googleResults = get_google_context(input, googleSearchResults)
history = llmChat_history.get(nodeID, ["", ""])
@@ -147,20 +271,11 @@ def llm_query(input, nodeID=0, location_name=None):
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
# Query the model via Ollama web API
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
# Condense the result to just needed
if result.status_code == 200:
result_json = result.json()
result = result_json.get("response", "")
# deepseek-r1 has added <think> </think> tags to the response
if "<think>" in result:
result = result.split("</think>")[1]
else:
raise Exception(f"HTTP Error: {result.status_code}")
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."
@@ -171,15 +286,8 @@ def llm_query(input, nodeID=0, location_name=None):
#retryy loop to truncate the response
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
if truncateResult.status_code == 200:
truncate_json = truncateResult.json()
result = truncate_json.get("response", "")
truncateResult = send_ollama_query(truncateQuery)
else:
#use the original result if truncation fails
logger.warning("System: LLM Query: Truncation failed, using original response")
# cleanup for message output
response = result.strip().replace('\n', ' ')

View File

@@ -16,6 +16,7 @@ trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert
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")
@@ -171,6 +172,7 @@ def getArtSciRepeaters(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
@@ -235,6 +237,7 @@ def get_NOAAtide(lat=0, lon=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
@@ -292,9 +295,36 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
return weather
def abbreviate_noaa(row):
# replace long strings with shorter ones for display
replacements = {
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",
@@ -310,6 +340,8 @@ def abbreviate_noaa(row):
"south": "S",
"east": "E",
"west": "W",
"accumulation": "accum",
"visibility": "vis",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms",
@@ -331,21 +363,31 @@ def abbreviate_noaa(row):
"degrees": "°",
"percent": "%",
"department": "Dept.",
"amounts less than a tenth of an inch possible.": "< 0.1in",
"temperatures": "temps.",
"temperature": "temp.",
"temperatures": "temps:",
"temperature": "temp:",
"amounts": "amts:",
"afternoon": "Aftn",
"evening": "Eve",
}
line = row
for key, value in replacements.items():
# case insensitive replace
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
return line
text = data
# 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:
@@ -422,6 +464,7 @@ def alertBrodcastNOAA():
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
@@ -609,54 +652,47 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
return alert
def get_flood_noaa(lat=0, lon=0, uid=0):
# get the latest flood alert from NOAA
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 uid == 0:
return "No flood gauge data found"
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("Location:Error fetching flood gauge data from NOAA for " + str(uid))
logger.warning(f"Location:Error fetching flood gauge data from NOAA for {uid} (HTTP {response.status_code})")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
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
data = response.json()
if not data:
return "No flood gauge data found"
# extract values from JSON
try:
name = data['name']
status_observed_primary = data['status']['observed']['primary']
status_observed_primary_unit = data['status']['observed']['primaryUnit']
status_observed_secondary = data['status']['observed']['secondary']
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
status_observed_floodCategory = data['status']['observed']['floodCategory']
status_forecast_primary = data['status']['forecast']['primary']
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
status_forecast_secondary = data['status']['forecast']['secondary']
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
# except KeyError as e:
# print(f"Missing key in data: {e}")
# except TypeError as e:
# print(f"Type error in data: {e}")
except Exception as e:
logger.debug("Location:Error extracting flood gauge data from NOAA for " + str(uid))
logger.warning(f"Location:Unexpected error: {e}")
return ERROR_FETCHING_DATA
# format the flood data
logger.debug(f"System: NOAA Flood data for {str(uid)}")
flood_data = f"Flood Data {name}:\n"
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
return flood_data
# 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 = ''
@@ -820,6 +856,7 @@ 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:

View File

@@ -1,7 +1,7 @@
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:

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

104
modules/scheduler.py Normal file
View File

@@ -0,0 +1,104 @@
# modules/scheduler.py 2025 meshing-around
import schedule
from modules.log import logger
from modules.system import send_message, BroadcastScheduler
from modules.system import send_message
# methods available for custom scheduler messages
from mesh_bot import tell_joke, welcome_message, MOTD, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
async def setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler
):
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
if 'custom' not in schedulerValue:
# 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}")
else:
# Default schedule if no valid configuration is provided
# custom scheduler job to run the schedule see examples below
logger.debug(f"System: Starting the scheduler 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))
# 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

@@ -28,6 +28,23 @@ wiki_return_limit = 3 # limit the number of sentences returned off the first par
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()
@@ -207,9 +224,10 @@ try:
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
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)
@@ -223,6 +241,9 @@ try:
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
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
llmModel = config['general'].get('ollamaModel', 'gemma3:270m') # default gemma3:270m
@@ -232,6 +253,11 @@ try:
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)
@@ -254,11 +280,14 @@ try:
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
@@ -341,25 +370,38 @@ try:
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('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
news_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['games'].getint('game_hop_limit', 5) # default 5 hops
@@ -373,18 +415,24 @@ try:
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
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
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}")

194
modules/survey.py Normal file
View File

@@ -0,0 +1,194 @@
# 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\nSend answer' or 'end'\n"
msg += self.show_question(user_id)
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:
row = list(map(str, self.responses[user_id]['answers']))
if surveyRecordID:
row.insert(0, str(user_id))
if surveyRecordLocation:
location = self.responses[user_id].get('location')
row.insert(1 if surveyRecordID else 0, str(location) if location is not None else "N/A")
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']):
self.responses[user_id]['answers'].append(str(option_index))
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

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

@@ -353,7 +353,6 @@ def onReceive(packet, interface):
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)
time.sleep(responseDelay)
# log the message to the message log
if log_messages_to_file:
@@ -432,28 +431,29 @@ async def start_rx():
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 solar_conditions_enabled:
logger.debug("System: Celestial Telemetry Enabled")
if motd_enabled:
logger.debug(f"System: MOTD Enabled using {MOTD}")
if enableEcho:
logger.debug(f"System: Echo command Enabled")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if store_forward_enabled:
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
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 file_monitor_enabled:
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
if read_news_enabled:
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
if 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/

View File

@@ -3,9 +3,32 @@
# 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:
@@ -14,23 +37,82 @@ try:
from modules.log import *
from modules.system import *
except Exception as e:
print(f"Error importing modules run this program from the main program directory 'python3 script/addFav.py'")
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:
# compile the favorite list wich returns node,interface tuples
favList = compileFavoriteList()
logger.debug(f"addFav: Compiled favorite list:\n {favList}")
# 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:
handleFavoritNode(fav['deviceID'], fav['nodeID'], True)
time.sleep(1)
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:
@@ -40,4 +122,9 @@ else:
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)

View File

@@ -24,6 +24,13 @@ if systemctl is-active --quiet mesh_bot_w3.service; then
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
@@ -37,22 +44,31 @@ if ! git pull origin main --rebase; then
fi
fi
# Install or update dependencies
echo "Installing or updating dependencies..."
if pip install -r requirements.txt --upgrade 2>&1 | grep -q "externally-managed-environment"; then
# if venv is found ask to run with launch.sh
if [ -d "venv" ]; then
echo "A virtual environment (venv) was found. run from inside venv"
# 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
read -p "Warning: You are in an externally managed environment. Do you want to continue with --break-system-packages? (y/n): " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
pip install --break-system-packages -r requirements.txt --upgrade
else
echo "Update aborted due to dependency installation issue."
fi
echo "Configuration merge completed. Please review config_new.ini and ini_merge_log.txt."
fi
else
echo "Dependencies installed or updated."
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