Compare commits

...

358 Commits

Author SHA1 Message Date
SpudGunMan
54837884a7 Update locationdata.py 2025-10-23 15:48:08 -07:00
Kelly
91501d42db Merge pull request #158 from pcs3rd/create-docker-image
Create docker image and push to packages on new release
2025-10-23 15:44:57 -07:00
SpudGunMan
e0bcc31204 Update README.md 2025-10-23 15:37:43 -07:00
SpudGunMan
1cb56aa1b7 set bbslink via config.ini 2025-10-23 15:32:10 -07:00
SpudGunMan
8cf2db3b49 Update custom_scheduler.py 2025-10-23 15:22:05 -07:00
SpudGunMan
755fc4fac3 Update joke.py 2025-10-23 14:51:14 -07:00
SpudGunMan
7c341ed0e7 cleanup 2025-10-23 14:50:18 -07:00
SpudGunMan
c87db75f2f cleanupPartOne
I hope I didnt break a lot of things!
2025-10-23 13:46:36 -07:00
SpudGunMan
13875b7cf8 Update pong_bot.py 2025-10-23 13:33:17 -07:00
SpudGunMan
fd4925ee92 Update mesh_bot.py 2025-10-23 13:18:31 -07:00
SpudGunMan
eccc48ff3f whats all this 2025-10-23 13:15:51 -07:00
SpudGunMan
3a6d464398 Update bbstools.md 2025-10-23 12:52:54 -07:00
SpudGunMan
6c7e8558b0 Update bbstools.md 2025-10-23 12:51:01 -07:00
SpudGunMan
74a744c77e Update wiki.py 2025-10-23 12:45:12 -07:00
SpudGunMan
5225998c92 Update rss.py 2025-10-23 12:43:33 -07:00
SpudGunMan
97a0ff3112 enhance 2025-10-23 12:41:37 -07:00
SpudGunMan
1250479219 Update README.md 2025-10-23 12:33:17 -07:00
SpudGunMan
f8bcc4f495 Update README.md 2025-10-23 12:26:34 -07:00
SpudGunMan
5d1608f366 Update README.md 2025-10-23 12:25:16 -07:00
SpudGunMan
a19dc93350 Update locationdata.py 2025-10-23 12:13:25 -07:00
SpudGunMan
30d4e487c9 Update locationdata.py 2025-10-23 12:12:54 -07:00
SpudGunMan
5bf1ade2b0 enhance 2025-10-23 12:10:45 -07:00
SpudGunMan
13cefc2002 Update locationdata.py 2025-10-23 12:08:00 -07:00
SpudGunMan
640bead32c Update locationdata.py 2025-10-23 12:06:45 -07:00
SpudGunMan
49d9b58627 Update mesh_bot.py 2025-10-23 12:02:55 -07:00
SpudGunMan
ad1a8aa1ce Update locationdata.py 2025-10-23 12:01:56 -07:00
SpudGunMan
55567815ef map
to csv
2025-10-23 11:57:55 -07:00
SpudGunMan
346fb38bbd heasder csv 2025-10-23 11:40:24 -07:00
SpudGunMan
104e70c01c Update README.md 2025-10-23 11:33:55 -07:00
SpudGunMan
2111bb46ae bbs doc 2025-10-23 10:27:28 -07:00
Kelly
459dad4c32 Merge branch 'main' into create-docker-image 2025-10-23 09:43:52 -07:00
SpudGunMan
d9febeef0f cleanupX2 2025-10-23 09:39:21 -07:00
SpudGunMan
f8a94fca71 cleanup
yikes this was messy
2025-10-23 09:21:48 -07:00
SpudGunMan
a30b3dc2d2 Update dependabot.yml
https://github.com/SpudGunMan/meshing-around/pull/228
2025-10-23 08:38:09 -07:00
Kelly
b45254795d Merge pull request #228 from SpudGunMan/dependabot/docker/python-3.14-slim
Bump python from 3.13-slim to 3.14-slim
2025-10-23 08:37:41 -07:00
SpudGunMan
4a209e0c17 Update greetings.yml 2025-10-23 08:30:14 -07:00
dependabot[bot]
1aecb42186 Bump python from 3.13-slim to 3.14-slim
Bumps python from 3.13-slim to 3.14-slim.

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 15:29:14 +00:00
SpudGunMan
6513e9f177 Update dependabot.yml 2025-10-23 08:28:22 -07:00
SpudGunMan
dd14034f3c Update update.sh 2025-10-23 00:20:45 -07:00
SpudGunMan
8308c2f98c Update README.md 2025-10-22 23:44:02 -07:00
SpudGunMan
5050f1c5eb Update install.sh 2025-10-22 23:43:20 -07:00
SpudGunMan
553137d228 Update install.sh 2025-10-22 23:41:43 -07:00
SpudGunMan
d3e7a4d5e4 Update install.sh 2025-10-22 23:25:11 -07:00
SpudGunMan
2e26819c2f Update install.sh 2025-10-22 22:40:14 -07:00
SpudGunMan
f03b85cebe typoFix
https://github.com/SpudGunMan/meshing-around/issues/227
thanks @mesb1
2025-10-22 22:32:53 -07:00
SpudGunMan
99f25345e8 Update README.md 2025-10-22 22:19:54 -07:00
SpudGunMan
4952bb3ecc documentation 2025-10-22 22:12:22 -07:00
SpudGunMan
f79f714317 enhance 2025-10-22 22:00:53 -07:00
SpudGunMan
a6db9bc878 Update README.md 2025-10-22 21:41:56 -07:00
SpudGunMan
774f76ecf1 Update README.md 2025-10-22 21:33:25 -07:00
SpudGunMan
3038d72996 Update README.md 2025-10-22 21:29:42 -07:00
SpudGunMan
33245e8443 Update README.md 2025-10-22 21:23:08 -07:00
SpudGunMan
54fb30d048 Update README.md 2025-10-22 21:20:27 -07:00
SpudGunMan
86c86d2f97 Update README.md 2025-10-22 21:18:23 -07:00
SpudGunMan
bb46981a85 Update README.md 2025-10-22 21:17:24 -07:00
SpudGunMan
e32cdf803c Update scheduler.py 2025-10-22 21:09:21 -07:00
SpudGunMan
c6e8feefd7 Update scheduler.py 2025-10-22 21:08:55 -07:00
SpudGunMan
1f48e9a4aa Create INSTALL.md 2025-10-22 21:03:55 -07:00
SpudGunMan
571c9c521f documentation 2025-10-22 21:02:12 -07:00
SpudGunMan
52b88ce16b documentation 2025-10-22 20:53:55 -07:00
SpudGunMan
6f38bff473 Update README.md 2025-10-22 20:31:20 -07:00
SpudGunMan
2b964390f8 Update README.md 2025-10-22 20:30:46 -07:00
SpudGunMan
a305acc492 Update README.md 2025-10-22 20:22:42 -07:00
SpudGunMan
cf66556fe6 enhance
fix bingo play
2025-10-22 20:21:17 -07:00
SpudGunMan
76e9bd8677 Update system.py 2025-10-22 18:56:49 -07:00
SpudGunMan
c8c3c0f80b Update wodt.py 2025-10-22 18:54:14 -07:00
SpudGunMan
3a9330d831 Update wodt.py 2025-10-22 18:47:16 -07:00
SpudGunMan
8d3b0ce4bf Update system.py
ahh
2025-10-22 18:46:50 -07:00
SpudGunMan
bb0a22c69b Update system.py 2025-10-22 18:45:45 -07:00
SpudGunMan
df369c3d29 Update system.py 2025-10-22 18:40:19 -07:00
SpudGunMan
db1ca48f0a Update system.py 2025-10-22 18:37:22 -07:00
SpudGunMan
a38d0a6ed7 Update system.py 2025-10-22 18:32:49 -07:00
SpudGunMan
ac8d308f58 Update system.py 2025-10-22 18:30:05 -07:00
SpudGunMan
08b54d9009 Update README.md 2025-10-22 18:25:49 -07:00
SpudGunMan
9da6416433 wordOfTheDay Games
Simple word fun, bingo and customizable word of the day lists via JSON files. Slot Machine for Emoji 🎰 shout out to pee-wee
2025-10-22 18:06:25 -07:00
SpudGunMan
efcfb749dc Update settings.py 2025-10-22 16:44:42 -07:00
SpudGunMan
493d2792d6 Update config.template 2025-10-22 16:44:31 -07:00
SpudGunMan
aa68ce120e Update scheduler.py 2025-10-22 16:17:27 -07:00
SpudGunMan
e3a9a00c92 better custom_ handling
thanks @pdxlocations for the idea
2025-10-22 14:54:06 -07:00
SpudGunMan
f1feb1be0d Update mesh_bot.py 2025-10-22 12:53:30 -07:00
SpudGunMan
290695327d enhance Ping with ping @NodeShortID
you should be able to now ping @ and if its a node ID it will send a BBS mail with a joke in it

@mesb1 thanks for idea
https://github.com/SpudGunMan/meshing-around/issues/224
2025-10-22 12:31:25 -07:00
SpudGunMan
c3ea07fde5 enhance
https://github.com/SpudGunMan/meshing-around/issues/225

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

https://github.com/SpudGunMan/meshing-around/issues/224
2025-10-22 11:48:04 -07:00
SpudGunMan
e551f1252a Update injectDM.py 2025-10-22 08:39:24 -07:00
Raymond Dean
57093d09ef Merge branch 'SpudGunMan:main' into create-docker-image 2025-10-22 09:43:27 -04:00
SpudGunMan
a8b2aefa28 Update locationdata.py 2025-10-21 22:31:57 -07:00
SpudGunMan
849565cacb Update joke.py 2025-10-21 21:40:20 -07:00
SpudGunMan
fdec3a6754 Update README.md 2025-10-21 20:56:47 -07:00
SpudGunMan
f3a97bc567 Update README.md 2025-10-21 20:45:40 -07:00
SpudGunMan
02625ad0f2 Update install.sh 2025-10-21 20:18:55 -07:00
SpudGunMan
b4ba4b0daf Update locationdata.py 2025-10-21 18:52:40 -07:00
SpudGunMan
fd7f8a94f5 Update locationdata.py 2025-10-21 18:47:29 -07:00
SpudGunMan
d252250edd last_alert_time 🚀
throttle sending alerts for the same node more than once every 30 minutes
2025-10-21 16:42:36 -07:00
SpudGunMan
d6410e0461 Update joke.py 2025-10-21 15:47:06 -07:00
SpudGunMan
050b4ab3ce Update joke.py 2025-10-21 15:46:48 -07:00
SpudGunMan
8ac1a1eed7 Update joke.py 2025-10-21 15:45:30 -07:00
SpudGunMan
370a417ce6 Update config.template 2025-10-21 15:10:13 -07:00
SpudGunMan
378b05df35 PingRefactor
anyone notice this?
2025-10-21 14:50:06 -07:00
SpudGunMan
d002c5ede8 remove LastHop 2025-10-21 14:26:43 -07:00
SpudGunMan
cd03cc56b4 🐇 2025-10-21 14:24:24 -07:00
SpudGunMan
d4fd484706 sentry_alert.sh
this enhances the sentry to optionally run a shell command you would create in the script/directory which will fire every time the alert fires. sentry_alert_near.sh and sentry_alert_far.sh are the needed files. it will error and remind you it cant find them.
2025-10-21 14:21:09 -07:00
SpudGunMan
82d519279e servicePackAttack
enhance for armbian builds
2025-10-21 13:34:38 -07:00
SpudGunMan
09302e8c91 ntp 2025-10-21 13:17:15 -07:00
SpudGunMan
91fc4605ec enhance 2025-10-21 12:57:10 -07:00
SpudGunMan
abc6c07ee3 Update locationdata.py 2025-10-21 12:52:34 -07:00
SpudGunMan
bbf8b04bd3 Update locationdata.py 2025-10-21 12:45:29 -07:00
SpudGunMan
5fd293c990 only seen with soft nodes
## FIXME needs better like a default interface setting or hash lookup
2025-10-21 12:33:13 -07:00
SpudGunMan
a9a65a6c6d refactor rxInt 2025-10-21 11:00:26 -07:00
SpudGunMan
34e95c86d6 log IP if there 2025-10-21 10:52:28 -07:00
Kelly
80891090c3 Merge pull request #222 from pdxlocations/allow-port-numbers
Missed a Spot to support hostname:port
thanks!
2025-10-21 10:49:00 -07:00
SpudGunMan
1b098fbf7b Update pong_bot.py 2025-10-21 10:47:42 -07:00
SpudGunMan
165d76cf8d add IP 2025-10-21 10:45:44 -07:00
Kelly
045c9d433b Merge pull request #223 from SpudGunMan/copilot/add-pylint-disable-comment 2025-10-21 10:28:51 -07:00
SpudGunMan
fbe5e008de Revert "Update joke.py"
This reverts commit 004adc7d9a.
2025-10-21 10:28:02 -07:00
Kelly
004adc7d9a Update joke.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 10:23:12 -07:00
copilot-swe-agent[bot]
9a2033452f Add Pylint disable comment above variable on line 58
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-21 17:08:51 +00:00
copilot-swe-agent[bot]
5638204f82 Initial plan 2025-10-21 17:05:27 +00:00
pdxlocations
e5c3b0cceb refactor 2025-10-21 09:33:01 -07:00
pdxlocations
18ac53b230 Refactor TCP interface handling to support hostname:poert 2025-10-21 09:17:54 -07:00
SpudGunMan
4aa65dad6a Update mesh_bot.py
https://github.com/SpudGunMan/meshing-around/issues/220
2025-10-21 09:03:00 -07:00
SpudGunMan
f65a7b7934 refactorMwx 2025-10-21 06:56:03 -07:00
SpudGunMan
886293087a Update greetings.yml 2025-10-20 23:10:25 -07:00
SpudGunMan
e05e6f3451 Update greetings.yml 2025-10-20 23:07:31 -07:00
SpudGunMan
4b5dd934e9 enhance 2025-10-20 23:07:03 -07:00
Kelly
008ddfb5a2 Merge pull request #221 from SpudGunMan/lab
DependaBot
2025-10-20 23:02:13 -07:00
SpudGunMan
e1330b9b9e DependaBot
DependaBot
2025-10-20 22:56:34 -07:00
SpudGunMan
7b43213094 Update README.md 2025-10-20 21:57:46 -07:00
SpudGunMan
b17c2b17ee Update system.py 2025-10-20 20:20:25 -07:00
SpudGunMan
b6505ee577 Update system.py 2025-10-20 20:19:43 -07:00
SpudGunMan
f7379b7ca5 Update system.py 2025-10-20 20:19:30 -07:00
SpudGunMan
ec9a1d88db Update system.py 2025-10-20 20:15:29 -07:00
SpudGunMan
a339570afe Update system.py 2025-10-20 20:10:08 -07:00
SpudGunMan
f5af9f419a Update system.py 2025-10-20 20:06:39 -07:00
SpudGunMan
ad5c1c90da Update system.py 2025-10-20 19:55:57 -07:00
SpudGunMan
eb1e0c82ea enhance Sentinel
# list of watched nodes numbers
sentryWatchList =
monitors for INSIDE or OUTSIDE the zone
2025-10-20 19:50:06 -07:00
SpudGunMan
8d2277bc59 Update SECURITY.md 2025-10-20 17:46:57 -07:00
Kelly
dc9908a72c Revise security support information and reporting process
Updated the security support table and reporting guidelines.
2025-10-20 17:08:31 -07:00
SpudGunMan
21123d2993 sysinfo from a DM
now lets you see the IP addresses of the bot
2025-10-20 16:33:58 -07:00
SpudGunMan
7dc3134d0b Update system.py 2025-10-20 16:20:27 -07:00
SpudGunMan
b125178492 Update survey.py 2025-10-20 16:16:49 -07:00
SpudGunMan
2ad9e84c33 Update pong_bot.py 2025-10-20 15:55:30 -07:00
SpudGunMan
63bd288caa fixMOTD
theMOTD is how did this happen
2025-10-20 15:40:32 -07:00
SpudGunMan
5c7d199831 surveyReport
run survey report to see return data
2025-10-20 15:34:09 -07:00
SpudGunMan
f56a39eeb6 refactor 2025-10-20 13:07:30 -07:00
SpudGunMan
ae5991ee39 moreTime
to spare
2025-10-20 13:03:33 -07:00
SpudGunMan
1324f83f17 enhance schedule
allow basic Joke and Weather messages with out extra config
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
2025-10-20 12:07:22 -07:00
SpudGunMan
08ae8c31a0 enhance:fix
I added some things to help any future aarg, sorry I broke it again I also
2025-10-20 11:45:49 -07:00
SpudGunMan
957e803951 cleanup 2025-10-20 11:35:26 -07:00
SpudGunMan
2de3441d67 enhance
with time stamp and also better CSV answers for review
2025-10-20 09:54:44 -07:00
SpudGunMan
2b420022f9 timeFix
will be make year 2000
2025-10-20 09:54:25 -07:00
SpudGunMan
d01f143adf Update system.py 2025-10-20 09:24:18 -07:00
SpudGunMan
5f5aeeadac Update system.py 2025-10-20 09:22:45 -07:00
SpudGunMan
d57826613c moved
to slurp repo
2025-10-20 09:19:18 -07:00
SpudGunMan
af09dc0cf9 improveUse 2025-10-19 22:43:51 -07:00
SpudGunMan
011bac41f2 lower 2025-10-19 22:32:06 -07:00
SpudGunMan
20467ea886 enhance 2025-10-19 22:30:40 -07:00
SpudGunMan
bbfd71f011 "IP-Network" 2025-10-19 22:27:40 -07:00
SpudGunMan
e1ff87a197 enhance Ping 2025-10-19 22:26:09 -07:00
SpudGunMan
a859f830bb coreFix
Enhance Packet hop and MQTT detection
2025-10-19 22:16:19 -07:00
SpudGunMan
d99698e7f3 Update blackjack.py 2025-10-19 20:25:36 -07:00
SpudGunMan
5ecc563e96 newChunker 2025-10-19 20:18:08 -07:00
SpudGunMan
eeeb43cacc Update lemonade.py 2025-10-19 20:12:28 -07:00
SpudGunMan
9fdcea56fc Update lemonade.py 2025-10-19 20:10:26 -07:00
SpudGunMan
24a33fe882 Update lemonade.py 2025-10-19 20:08:34 -07:00
SpudGunMan
5710cebf39 Update mmind.py 2025-10-19 20:04:05 -07:00
SpudGunMan
b66487863d Update mmind.py 2025-10-19 19:35:41 -07:00
SpudGunMan
b3c4d208b7 Update mmind.py 2025-10-19 19:29:34 -07:00
SpudGunMan
f41ff2d5f7 Update mmind.py 2025-10-19 19:21:48 -07:00
SpudGunMan
48366bc595 Update mmind.py 2025-10-19 19:15:31 -07:00
SpudGunMan
02dd64382d Update mesh_bot.py 2025-10-19 19:05:23 -07:00
SpudGunMan
731b48ad65 Update mesh_bot.py 2025-10-19 19:04:34 -07:00
SpudGunMan
69a7082669 Update README.md 2025-10-19 18:57:56 -07:00
SpudGunMan
fafa7d8a51 Update config.template 2025-10-19 18:57:27 -07:00
Kelly
6e69b5f014 Merge pull request #219 from pdxlocations/allow-port-numbers
Enhance TCP interface initialization to support host:port format
2025-10-19 18:54:54 -07:00
SpudGunMan
03895248cd fixing
sorry if you saw the crashing I had dinner
2025-10-19 18:31:51 -07:00
SpudGunMan
a79de8a325 cleanupBadCode 2025-10-19 17:16:53 -07:00
SpudGunMan
740b53f02f Update system.py 2025-10-19 16:25:35 -07:00
SpudGunMan
76e75551c6 Update videopoker.py 2025-10-19 16:15:49 -07:00
SpudGunMan
51752ae896 Update videopoker.py 2025-10-19 16:15:37 -07:00
SpudGunMan
d81e773c0c Update blackjack.py 2025-10-19 16:02:24 -07:00
SpudGunMan
1f1ed1ca70 Update blackjack.py 2025-10-19 16:01:04 -07:00
pdxlocations
df9f3806a3 Enhance TCP interface initialization to support host:port format 2025-10-19 16:00:45 -07:00
SpudGunMan
081ccd9e2e Update blackjack.py 2025-10-19 15:55:46 -07:00
SpudGunMan
d9a7dafe6e Update blackjack.py 2025-10-19 15:54:41 -07:00
SpudGunMan
921225965b Update blackjack.py 2025-10-19 15:52:20 -07:00
SpudGunMan
3659254785 refactor suggestion 2025-10-19 15:42:49 -07:00
SpudGunMan
7c502608f6 remove deps install 2025-10-19 14:51:51 -07:00
SpudGunMan
427c25f80b noHoldsHeld 2025-10-19 14:26:45 -07:00
SpudGunMan
c3f15390ea Update system.py 2025-10-19 14:08:21 -07:00
SpudGunMan
e1476a44c6 enhance 2025-10-19 13:46:58 -07:00
SpudGunMan
72070fef3e backup data 2025-10-19 13:43:57 -07:00
SpudGunMan
b63ea677f6 Update blackjack.py 2025-10-19 13:35:00 -07:00
SpudGunMan
f8389500b8 Update mesh_bot.py 2025-10-19 13:28:32 -07:00
SpudGunMan
b257625a45 cleanupBlackJack 2025-10-19 13:23:40 -07:00
SpudGunMan
a233d8c7b3 Update mesh_bot.py 2025-10-19 12:57:37 -07:00
SpudGunMan
11c9742ebe cleanup 2025-10-19 12:55:16 -07:00
SpudGunMan
5af28c3dc2 Update system.py
kidding
2025-10-19 12:55:08 -07:00
SpudGunMan
aebb9e3c20 cleanup 2025-10-19 12:48:19 -07:00
SpudGunMan
d5916f4ccc 🐞🧩
thanks meshguy
2025-10-19 12:22:09 -07:00
SpudGunMan
056159a3f3 Update mesh_bot.py 2025-10-19 09:21:25 -07:00
SpudGunMan
2f6049d94b bugfix survey game 2025-10-18 19:20:36 -07:00
SpudGunMan
a2d7f664ab Update udp.py 2025-10-18 17:29:21 -07:00
SpudGunMan
b26491b646 Update udp.py 2025-10-18 17:26:39 -07:00
SpudGunMan
22e97b0eec Update udp.py 2025-10-18 16:54:26 -07:00
SpudGunMan
f540866d08 Update locationdata.py 2025-10-18 15:18:18 -07:00
SpudGunMan
c9729c8214 Update locationdata.py 2025-10-18 15:17:34 -07:00
SpudGunMan
49901cbbee Update locationdata.py 2025-10-18 15:14:23 -07:00
SpudGunMan
2aa2b80935 Update locationdata.py 2025-10-18 15:08:40 -07:00
SpudGunMan
95695f4f58 Update locationdata.py 2025-10-18 15:02:33 -07:00
SpudGunMan
b641d2b5e8 ok finslly
this looks better
2025-10-18 14:54:45 -07:00
SpudGunMan
51d8faab12 enhance 2025-10-18 14:49:26 -07:00
SpudGunMan
7a1396b99d Update locationdata.py 2025-10-18 14:40:39 -07:00
SpudGunMan
819bbbcaf4 enhance 2025-10-18 14:39:06 -07:00
SpudGunMan
0eeda96670 Update locationdata.py 2025-10-18 14:36:44 -07:00
SpudGunMan
18cca4ffdd Update locationdata.py 2025-10-18 14:35:28 -07:00
SpudGunMan
d169fe2dff Update locationdata.py 2025-10-18 14:33:42 -07:00
SpudGunMan
1c732dfe17 Update install.sh 2025-10-18 12:47:17 -07:00
SpudGunMan
bdad3927e5 enhance 2025-10-18 10:07:09 -07:00
SpudGunMan
0e0d6416d9 enhance config merge data 2025-10-18 09:38:00 -07:00
SpudGunMan
0da780371a enhance 2025-10-18 09:10:47 -07:00
SpudGunMan
37bf30cbc0 enhance 2025-10-18 09:05:17 -07:00
SpudGunMan
817a8601dd Update system.py 2025-10-18 08:53:30 -07:00
SpudGunMan
47cca409be lab work 2025-10-18 08:52:32 -07:00
SpudGunMan
e08a82ec39 Update system.py 2025-10-18 08:42:48 -07:00
SpudGunMan
345541dfb5 Update system.py 2025-10-18 08:41:22 -07:00
SpudGunMan
6e89762f1d bbsCompression
not enabled yet
2025-10-18 08:39:43 -07:00
SpudGunMan
0fb26bc16a Update mesh_bot.py 2025-10-17 19:50:47 -07:00
SpudGunMan
f1ad5966af send_raw_bytes 2025-10-17 19:50:41 -07:00
SpudGunMan
ac57d4683f Update udp.py 2025-10-17 17:48:24 -07:00
SpudGunMan
eab099e5ee channelID 2025-10-17 17:42:07 -07:00
SpudGunMan
685bd3491d Update udp.py 2025-10-17 17:10:44 -07:00
SpudGunMan
b8d64f3a9e Update system.py 2025-10-17 13:31:47 -07:00
SpudGunMan
852d491030 Update meshview.ino 2025-10-16 18:57:17 -07:00
SpudGunMan
76565c5546 Update meshview.ino 2025-10-16 18:55:03 -07:00
SpudGunMan
af1ec1630e Update udp.py 2025-10-16 16:04:06 -07:00
SpudGunMan
0c2b36a206 refactor handle_messages
@mesb1 give this one a test

https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-16 15:55:12 -07:00
SpudGunMan
c0934096f0 Update meshview.ino 2025-10-16 12:07:58 -07:00
SpudGunMan
819bfaba90 Update meshview.ino 2025-10-16 11:52:03 -07:00
SpudGunMan
8041a1296b tinkering
@martinbogo
2025-10-15 20:32:46 -07:00
SpudGunMan
10d93b4fd3 keyFactor 2025-10-15 20:17:44 -07:00
SpudGunMan
19dedef1e6 meshview.ino
I could use help with this I am stuck at the moment
2025-10-15 19:25:21 -07:00
SpudGunMan
d4af0c7e8b Update udp.py 2025-10-15 15:58:02 -07:00
SpudGunMan
8730f0fd38 Update udp.py 2025-10-15 15:57:40 -07:00
SpudGunMan
9cda8daf65 Update udp.py 2025-10-15 15:57:24 -07:00
SpudGunMan
a9223f1613 Create udp.py 2025-10-15 15:51:30 -07:00
SpudGunMan
04ca4c99b8 Update scheduler.py
sorry for that
2025-10-15 08:24:02 -07:00
SpudGunMan
3072520e63 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-10-15 08:23:16 -07:00
SpudGunMan
bd6603766b Update scheduler.py 2025-10-15 08:23:14 -07:00
Kelly
075a23bd2b LowerBits
https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 22:21:21 -07:00
SpudGunMan
a8e4f653ed Update radio.py 2025-10-14 21:19:00 -07:00
SpudGunMan
374a44f4a9 Update radio.py 2025-10-14 21:17:36 -07:00
SpudGunMan
3c8d2e646e Update radio.py 2025-10-14 16:32:01 -07:00
SpudGunMan
e5df983244 Update mesh_bot.py 2025-10-14 16:23:27 -07:00
SpudGunMan
fa5f9250c4 Update llm.py 2025-10-14 16:14:59 -07:00
SpudGunMan
3f7a831690 Update llm.py 2025-10-14 16:12:14 -07:00
SpudGunMan
89aaaddae9 Update llm.py 2025-10-14 16:02:40 -07:00
SpudGunMan
e1919616c2 refactoring 2025-10-14 15:55:59 -07:00
SpudGunMan
8b9e637006 Update README.md 2025-10-14 15:08:07 -07:00
SpudGunMan
0df3e32901 Update README.md 2025-10-14 15:07:18 -07:00
SpudGunMan
1c2fa174ea Hey Chirpy
- **Voice/Command Triggers**: The following keywords can be used in messages or via voice (VOX) to trigger bot functions:
  - `joke`: Tells a joke
  - `weather`: Returns local weather forecast
  - `moon`: Returns moonrise/set and phase info
  - `daylight`: Returns sunrise/sunset times
  - `river`: Returns NOAA river flow info
  - `tide`: Returns NOAA tide information
  - `satellite`: Returns satellite pass info
2025-10-14 15:05:39 -07:00
SpudGunMan
c97aefcef1 Update radio.py 2025-10-14 14:59:10 -07:00
SpudGunMan
dfb94c3993 voxUse 2025-10-14 14:56:45 -07:00
SpudGunMan
7d62f69f12 ... 2025-10-14 14:24:42 -07:00
SpudGunMan
cf896767fb Update mesh_bot.py 2025-10-14 13:41:31 -07:00
SpudGunMan
1eb4cf71ed Update mesh_bot.py 2025-10-14 13:40:27 -07:00
SpudGunMan
e959124eac voxEnhance 2025-10-14 13:38:26 -07:00
SpudGunMan
d787c72812 Update radio.py 2025-10-14 13:32:22 -07:00
SpudGunMan
9f0dd56d43 Update mesh_bot.py 2025-10-14 13:26:37 -07:00
SpudGunMan
aa71e6045a Update radio.py
forgot to save a good
2025-10-14 12:45:41 -07:00
SpudGunMan
a140ad83cd Update radio.py 2025-10-14 12:34:44 -07:00
SpudGunMan
93c2d731e8 Update radio.py 2025-10-14 12:33:18 -07:00
SpudGunMan
d8da553af9 Update radio.py 2025-10-14 12:32:21 -07:00
SpudGunMan
9d9f070908 enhance 2025-10-14 12:24:42 -07:00
SpudGunMan
0f2061af55 chirpy make my lunch 2025-10-14 12:24:29 -07:00
SpudGunMan
d8423584d4 Update radio.py 2025-10-14 12:19:13 -07:00
SpudGunMan
843320d268 Update radio.py 2025-10-14 12:18:11 -07:00
SpudGunMan
216128b15a Update radio.py 2025-10-14 12:07:45 -07:00
SpudGunMan
f8bc574753 Update radio.py 2025-10-14 11:35:32 -07:00
SpudGunMan
6193c5933f Update radio.py 2025-10-14 11:32:34 -07:00
SpudGunMan
b668965bda bufferHandler
tracking https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 11:13:40 -07:00
SpudGunMan
ae039b5baf Update radio.py 2025-10-14 11:01:24 -07:00
SpudGunMan
824d43f16e Update radio.py 2025-10-14 10:57:39 -07:00
SpudGunMan
2de76e6c5e Update radio.py 2025-10-14 10:56:14 -07:00
SpudGunMan
afb02602fd Update radio.py 2025-10-14 10:53:44 -07:00
SpudGunMan
99528c2bcf Update system.py 2025-10-14 10:52:35 -07:00
SpudGunMan
b53f5821f3 Update system.py 2025-10-14 10:51:34 -07:00
SpudGunMan
93fc6547b8 Update system.py 2025-10-14 10:46:30 -07:00
SpudGunMan
9a7e321dff Update scheduler.py 2025-10-14 10:25:32 -07:00
SpudGunMan
39257f2d39 Update scheduler.py 2025-10-14 10:25:16 -07:00
SpudGunMan
8c5abecac3 refactor
or custom for module/scheduler.py
2025-10-14 10:09:33 -07:00
SpudGunMan
16dcc96037 consolidate time to wait 2025-10-14 09:38:38 -07:00
SpudGunMan
b1d32a7745 refactor MOTD 2025-10-14 09:22:41 -07:00
SpudGunMan
631a2f53ea major refactor to schedule
major thanks to @FJRPiolt
2025-10-14 08:44:59 -07:00
SpudGunMan
32903c97e3 enhance 2025-10-14 08:13:35 -07:00
SpudGunMan
6e61e8122d Update system.py
no its not
2025-10-14 08:07:00 -07:00
SpudGunMan
d109803f9d Update system.py 2025-10-14 07:21:05 -07:00
SpudGunMan
09ed4f57cf Update system.py
enhance high fly with block list blocks
2025-10-14 07:18:10 -07:00
SpudGunMan
acfb8078a9 Update system.py
to much log
2025-10-14 07:06:46 -07:00
SpudGunMan
84f9693833 Update system.py 2025-10-13 23:50:32 -07:00
SpudGunMan
50fdcf486d Update system.py 2025-10-13 23:21:50 -07:00
SpudGunMan
eab5afccc8 Update system.py
helps to hit save
2025-10-13 21:31:10 -07:00
SpudGunMan
ea9db47c2d refactor sysinfo local telemetry 2025-10-13 21:29:45 -07:00
SpudGunMan
cf3a9c5b43 Update filemon.py 2025-10-13 19:49:43 -07:00
SpudGunMan
adedaa092c Update mesh_bot.py
fixLocaStats and sysinfo
2025-10-13 19:49:24 -07:00
SpudGunMan
f204237a63 Update mesh_bot.py 2025-10-13 19:27:58 -07:00
SpudGunMan
057a400041 Update mesh_bot.py 2025-10-13 19:26:53 -07:00
SpudGunMan
4cdf68f074 fixLocaStats and sysinfo 2025-10-13 19:24:37 -07:00
SpudGunMan
003a11c557 fixReportingEngine
This data is used by the webReporting engine
2025-10-13 17:57:20 -07:00
SpudGunMan
8d309fa579 Update README.md 2025-10-13 17:42:35 -07:00
SpudGunMan
232f9c24db aaahhhrrg 2025-10-13 17:27:51 -07:00
SpudGunMan
39dccd149b Update mesh_bot.py 2025-10-13 17:26:45 -07:00
SpudGunMan
b921c73fa7 Update mesh_bot.py 2025-10-13 17:26:08 -07:00
SpudGunMan
f3ec1cbe93 enhance 2025-10-13 17:23:49 -07:00
SpudGunMan
a6bcfda0ac enhance 2025-10-13 17:20:56 -07:00
SpudGunMan
51cd2002af Update system.py 2025-10-13 17:13:37 -07:00
SpudGunMan
b40f41f41c bannode
bad node! this isnt saving to .ini
2025-10-13 17:12:27 -07:00
SpudGunMan
4c33b30f14 addMessageData
Co-Authored-By: Martin Bogomolni <martinbogo@igotu.com>
2025-10-13 15:22:29 -07:00
SpudGunMan
b7490afb99 Update llm.py 2025-10-13 15:03:42 -07:00
SpudGunMan
8b57ed727c Update mesh_bot.py 2025-10-13 13:50:07 -07:00
SpudGunMan
fd5d64b9fb 🫖
enhance
2025-10-13 13:14:32 -07:00
SpudGunMan
00af152c2c Update system.py
slowing this a bit
2025-10-13 12:28:41 -07:00
SpudGunMan
31f0abc8c8 requestPosition
alsoRequesting feedback if this works well? you will need to edit the file find the `reqLocationEnabled` and set True. save and test it out
2025-10-13 12:00:36 -07:00
SpudGunMan
6b7d795a31 Update README.md 2025-10-13 10:04:13 -07:00
SpudGunMan
1f093c4bc2 Update system.py 2025-10-13 10:02:22 -07:00
SpudGunMan
fe1c4a1ad0 Update locationdata.py 2025-10-13 10:02:20 -07:00
SpudGunMan
11687cb7ba ‼️UPDATE LOCATION🗺️
this is a fail safe change to fuzzing the default location. This may change the way you use the bot today and should evaluate the change specifically test the auto alerts for proper data for emergency alerts etc.`fuzzConfigLocation = True`
2025-10-13 09:49:10 -07:00
SpudGunMan
b07a7fb0cc Update radio.py 2025-10-13 08:40:21 -07:00
SpudGunMan
b876d87ba9 enhance 2025-10-13 08:38:27 -07:00
SpudGunMan
0a63e89633 waitTooLong!
haha I well sorry
2025-10-13 08:23:07 -07:00
SpudGunMan
848f5609c2 Update README.md 2025-10-12 23:22:33 -07:00
SpudGunMan
0ccbed6165 fix Lemons 2025-10-12 23:19:08 -07:00
SpudGunMan
646517db71 Update mesh_bot.py 2025-10-12 21:27:14 -07:00
SpudGunMan
7d347bb80a enhance 2025-10-12 21:24:58 -07:00
SpudGunMan
e199d4f5eb Update mesh_bot.py 2025-10-12 20:03:03 -07:00
SpudGunMan
a9767b58c4 Update mesh_bot.py 2025-10-12 20:00:22 -07:00
SpudGunMan
69dfde047e Update lemonade.py 2025-10-12 20:00:20 -07:00
SpudGunMan
da33b6f1b9 Update dopewar.py 2025-10-12 19:55:43 -07:00
SpudGunMan
8a7125358b Update lemonade.py 2025-10-12 18:23:14 -07:00
SpudGunMan
ae558052f7 hey chirpy
vox trapping
2025-10-12 18:17:05 -07:00
SpudGunMan
5074d71eb7 defaults 2025-10-12 17:22:02 -07:00
SpudGunMan
632f42477a Update settings.py 2025-10-12 17:18:44 -07:00
SpudGunMan
b3df38d15e Update radio.py
aaarg
2025-10-12 17:17:31 -07:00
SpudGunMan
b76b8ca718 Update radio.py 2025-10-12 17:17:02 -07:00
SpudGunMan
d66a9e745b enhance 2025-10-12 17:13:41 -07:00
SpudGunMan
717bbccea3 Omg 2025-10-12 16:25:43 -07:00
SpudGunMan
50fd1c0410 Update tictactoe.py 2025-10-12 16:22:25 -07:00
SpudGunMan
ae89788ea4 Update settings.py 2025-10-12 15:33:02 -07:00
SpudGunMan
4220b095ee Update addFav.py 2025-10-12 15:27:30 -07:00
SpudGunMan
ef28341cdb Update addFav.py 2025-10-12 15:07:27 -07:00
SpudGunMan
b5d610728c Update addFav.py
ffs
2025-10-12 15:03:44 -07:00
SpudGunMan
bc238ef476 Update addFav.py 2025-10-12 14:38:20 -07:00
SpudGunMan
feb3544014 fixBug 2025-10-12 14:32:59 -07:00
SpudGunMan
31322dc0cd lessWait
remove some waits now that 2 seconds is needed by firmware
2025-10-12 14:26:23 -07:00
Raymond Dean
29a26d5d14 Merge branch 'SpudGunMan:main' into create-docker-image 2025-08-08 19:29:53 -04:00
Raymond Dean
43e6349351 Update compose example. 2025-07-15 17:42:17 -04:00
Raymond Dean
628f66e4b7 Update compose.yaml to expose port 8420 2025-07-12 14:08:29 -04:00
Raymond Dean
29f97c62d0 add compose example 2025-07-12 11:50:21 -04:00
Raymond Dean
b805e6d428 Update docker-image.yml 2025-07-12 11:19:35 -04:00
Raymond Dean
6d5ded7df6 Create and publish a Docker image on new release 2025-07-12 11:18:05 -04:00
55 changed files with 5313 additions and 1912 deletions

28
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"

61
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
#
name: Create and publish a Docker image on new release
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
release:
types: [released]
workflow_dispatch:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

22
.github/workflows/greetings.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Greetings
on:
issues:
types: [opened]
pull_request:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
greeting:
name: Greet first-time contributors
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue_message: "Dependabot's first issue"
pr_message: "Thank you for your pull request!"

8
.gitignore vendored
View File

@@ -1,5 +1,7 @@
# config
config.ini
config_new.ini
ini_merge_log.txt
# Pickle files
*.pkl
@@ -27,4 +29,8 @@ data/qrz.db
bee.txt
# .csv files
*.csv
*.csv
# modules/custom_scheduler.py
modules/custom_scheduler.py

View File

@@ -1,21 +1,32 @@
FROM python:3.13-slim
FROM python:3.14-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/*
ENV PYTHONUNBUFFERED=1 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
TZ=America/Los_Angeles
# Set the locale default to en_US.UTF-8
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8
ENV LANG="en_US.UTF-8"
ENV TZ="America/Los_Angeles"
RUN apt-get update && \
apt-get install -y gettext tzdata locales nano && \
sed -i 's/^# *\(en_US.UTF-8 UTF-8\)/\1/' /etc/locale.gen && \
locale-gen en_US.UTF-8 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install dependencies first for better caching
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application
COPY . /app
COPY config.template /app/config.ini
RUN pip install -r requirements.txt
RUN chmod +x /app/script/docker/entrypoint.sh
# Add a non-root user and switch to it
RUN useradd -m appuser
USER appuser
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]

183
INSTALL.md Normal file
View File

@@ -0,0 +1,183 @@
# INSTALL.md
## Table of Contents
- [install.sh](#installsh)
- [Purpose](#purpose)
- [Usage](#usage)
- [What it does](#what-it-does)
- [When to use](#when-to-use)
- [Note](#note)
- [update.sh](#updatesh)
- [Purpose](#purpose-1)
- [Usage](#usage-1)
- [What it does](#what-it-does-1)
- [When to use](#when-to-use-1)
- [Note](#note-1)
- [launch.sh](#launchsh)
- [Purpose](#purpose-2)
- [How to Use](#how-to-use)
- [What it does](#what-it-does-2)
- [Note](#note-2)
---
### Manual Install
Install the required dependencies using pip:
```sh
pip install -r requirements.txt
```
Copy the configuration template to `config.ini` and edit it to suit your needs:
```sh
cp config.template config.ini
```
### Docker Installation - handy for windows
See further info on the [docker.md](script/docker/README.md)
### Requirements
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install schedule
pip install wikipedia
```
For the Ollama LLM:
```sh
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
## install.sh
### Purpose
`install.sh` is an installation and setup script for the Meshing Around Bot project. It automates installing dependencies, configuring the environment, setting up system services, and preparing the bot for use on Linux systems (especially Debian/Ubuntu/Raspberry Pi and embedded devices).
### Usage
Run this script from the project root directory:
```sh
bash install.sh
```
### What it does
- Checks for existing installations and required permissions.
- Optionally moves the project to `/opt/meshing-around` for standardization.
- Installs Python and pip if not present (unless on embedded systems).
- Adds the current user (or a dedicated `meshbot` user) to necessary groups for serial and Bluetooth access.
- Copies and configures systemd service files for running the bot as a service.
- Sets up configuration files, updating latitude/longitude automatically.
- Offers to create and activate a Python virtual environment, or install dependencies system-wide.
- Installs optional components (emoji fonts, Ollama LLM) if desired.
- Sets permissions for log and data directories.
- Optionally installs and enables the bot as a systemd service.
- Provides post-installation notes and commands in `install_notes.txt`.
- Offers to reboot the system to complete setup.
### When to use
- For first-time installation of the Meshing Around Bot.
- When migrating to a new device or environment.
- After cloning or updating the repository to set up dependencies and services.
### Note
- You may be prompted for input during installation (e.g., for embedded mode, virtual environment, or optional features).
- Review and edit the script if you have custom requirements or are running on a non-standard system.
---
## update.sh
### Purpose
`update.sh` is an update and maintenance script for the Meshing Around Bot project. It automates the process of safely updating your codebase, backing up data, and merging configuration changes.
### Usage
Run this script from the project root directory:
```sh
bash update.sh
```
Or, after making it executable:
```sh
chmod +x update.sh
./update.sh
```
### What it does
- Stops running Mesh Bot services to prevent conflicts during update.
- Fetches and pulls the latest changes from the GitHub repository (using `git pull --rebase`).
- Handles git conflicts, offering to reset to the latest remote version if needed.
- Copies a custom scheduler template if not already present.
- Backs up the `data/` directory (and `custom_scheduler.py` if present) to a compressed archive.
- Merges your existing configuration with new defaults using `script/configMerge.py`, and logs the process.
- Restarts services if they were stopped for the update.
- Provides status messages and logs for troubleshooting.
### When to use
- To update your Mesh Bot installation to the latest version.
- Before making significant changes or troubleshooting, as it creates a backup of your data.
### Note
- Review `ini_merge_log.txt` and `config_new.ini` after running for any configuration changes or errors.
- You may be prompted if git conflicts are detected.
---
## launch.sh
### Purpose
`launch.sh` is a convenience script for starting the Mesh Bot, Pong Bot, or generating reports within the Python virtual environment. It ensures the correct environment is activated and the appropriate script is run.
### How to Use
From your project root, run one of the following commands:
- Launch Mesh Bot:
```sh
bash launch.sh mesh
```
- Launch Pong Bot:
```sh
bash launch.sh pong
```
- Generate HTML report:
```sh
bash launch.sh html
```
- Generate HTML5 report:
```sh
bash launch.sh html5
```
- Add a favorite (calls `script/addFav.py`):
```sh
bash launch.sh add
```
### What it does
- Ensures you are in the project directory.
- Copies `config.template` to `config.ini` if no config exists.
- Activates the Python virtual environment (`venv`).
- Runs the selected Python script based on your argument.
- Deactivates the virtual environment when done.
### Note
- The script requires a Python virtual environment (`venv`) to be present in the project directory.
- If `venv` is missing, the script will exit with an error message.
- Always provide an argument (`mesh`, `pong`, `html`, `html5`, or `add`) to specify what you want to launch.

597
README.md
View File

@@ -1,90 +1,105 @@
# Mesh Bot for Network Testing and BBS Activities
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
Mesh Bot is a feature-rich Python bot designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience. It provides powerful tools for network testing, messaging, games, and more—all via text-based message delivery. Whether you want to test your mesh, send messages, or play games, [mesh_bot.py](mesh_bot.py) has you covered.
* [Getting Started](#getting-started)
![Example Use](etc/pong-bot.jpg "Example Use")
#### TLDR
* [install.sh](INSTALL.md)
* [Configuration Guide](modules/README.md)
* [Games Help](modules/games/README.md)
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
### Intelligent Keyword Responder
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
- **New Node Hello**: Greet new nodes on the mesh with a hello message
- **Automated Responses**: Detects keywords like "ping" and replies with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitors group channels for specific keywords and sends custom responses.
- **Emergency Detection**: Watches for emergency-related keywords and alerts a wider audience.
- **New Node Greetings**: Automatically welcomes new nodes joining the mesh.
### Network Tools
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
- **Network Monitoring**: Alert on noisy nodes, node locations, and best placment for relay nodes.
- **Mesh Testing**: Use `ping` to test message delivery with realistic packets.
- **Hardware Testing**: The `test` command sends incrementally sized data to test radio buffer limits.
- **Network Monitoring**: Alerts for noisy nodes, tracks node locations, and suggests optimal relay placement.
### Multi Radio/Node Support
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
- **Flexible Messaging**: send mail and messages, between networks.
- **Site Survey & Location Logging**: Use the `map` command to log your current GPS location with a custom description—ideal for site surveys, asset tracking, or mapping nodes locations. Entries are saved to a CSV file for later analysis or visualization.
### Multi-Radio/Node Support
- **Simultaneous Monitoring**: Observe up to nine networks at once.
- **Flexible Messaging**: Send mail and messages between networks.
### Advanced Messaging Capabilities
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
- **New Node Hello**: Send a hello to any new node seen in text message.
- **Mail Messaging**: Leave messages for other devices; delivered as DMs when the device is next seen. Use `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **Message Scheduler**: Automate messages such as weather updates or net reminders.
- **Store and Forward**: Retrieve missed messages with the `messages` command; optionally log messages to disk.
- **BBS Linking**: Connect multiple bots to expand BBS coverage.
- **E-Mail/SMS Integration**: Send mesh messages to email or SMS for broader reach.
- **New Node Greetings**: Automatically greet new nodes via text.
### Interactive AI and Data Lookup
- **NOAA/USGS location Data**: Get localized weather(alerts), Earthquake, River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **Wiki Integration**: Look up data using Wikipedia results.
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
- **Satellite Pass Info**: Get passes for satellite at your location.
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
- **Weather, Earthquake, River, and Tide Data**: Get local alerts and info from NOAA/USGS; uses Open-Meteo for areas outside NOAA coverage.
- **Wikipedia Search**: Retrieve summaries from Wikipedia.
- **Ollama LLM Integration**: Query the [Ollama](https://github.com/ollama/ollama/tree/main/docs) AI for advanced responses.
- **Satellite Passes**: Find upcoming satellite passes for your location.
- **GeoMeasuring Tools**: Calculate distances and midpoints using collected GPS data; supports Fox & Hound direction finding.
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
- **Location-Based Alerts**: Get notified when members arrive at a configured latitude/longitude—ideal for campsites, geo-fences, or remote locations. Optionally, trigger scripts, send emails, or automate actions (e.g., change node config, turn on lights, or drop an `alert.txt` file to start a survey or game).
- **Customizable Triggers**: Use proximity events for creative applications like "king of the hill" or 🧭 geocache games by adjusting the alert cycle.
- **High Flying Alerts**: Receive notifications when nodes with high altitude are detected on the mesh.
- **Voice/Command Triggers**: Activate bot functions using keywords or voice commands (see [Voice Commands](#voice-commands-vox) for "Hey Chirpy!" support).
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
#### Radio Frequency Monitoring
- **SNR RF Activity Alerts**: Monitor radio frequencies and receive alerts when high SNR (Signal-to-Noise Ratio) activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to monitor the S meter on a connected radio.
- **Speech-to-Text Broadcasting**: Convert received audio to text using [Vosk](https://alphacephei.com/vosk/models) and broadcast it to the mesh.
### Check-In / Check-Out & Asset Tracking
- **Asset Tracking**: Maintain a check-in/check-out list for nodes or assets—ideal for accountability of people and equipment (e.g., Radio-Net, FEMA, trailhead groups).
### Fun and Games
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
- **Telemetry Leaderboard**: Fun stats like lowest 🪫 battery or coldest temp 🥶
- **Built-in Games**: Play classic games like DopeWars, Lemonade Stand, BlackJack, and Video Poker directly via DM.
- **FCC ARRL QuizBot**: Practice for the ham radio exam with the integrated quiz bot.
- **Command-Based Gameplay**: Use the `games` command to view available games and start playing.
- **Telemetry Leaderboard**: Compete for fun stats like lowest battery or coldest temperature.
#### 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`.
- **Group Quizzes**: Admins can start and stop quiz games for groups.
- **Player Participation**: Players join with `q: join`, leave with `q: leave`, and answer questions by prefixing their answer with `q:`, e.g., `q: 42`.
- **Scoring & Leaderboards**: Check your score with `q: score` and see the top performers with `q: top`.
- **Admin Controls**: QuizMasters (from `bbs_admin_list`) can use `q: start`, `q: stop`, and `q: broadcast <message>` to manage games.
#### 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.
- **Custom Surveys**: Create and manage surveys by editing JSON files in `data/survey`. Multiple surveys are supported (e.g., `survey snow`).
- **User Feedback**: Users participate via DM; responses are logged for review.
- **Reporting**: Retrieve survey results with `survey report` or `survey report <surveyname>`.
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
- **FEMA iPAWS/EAS Alerts**: Receive Emergency Alerts from FEMA via API on internet-connected nodes.
- **NOAA EAS Alerts**: Get Emergency Alerts from NOAA via API.
- **USGS Volcano Alerts**: Receive volcano alerts from USGS via API.
- **Offline EAS Alerts**: Report EAS alerts over the mesh using external tools, even without internet.
- **NINA Alerts (Germany)**: Receive emergency alerts from the xrepository.de feed for Germany.
### 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. Can also call multiple news sources or files.
- **Shell Command Access**: Pass commands via DM directly to the host OS with replay protection.
- **File Monitoring**: Watch a text file for changes and broadcast updates to the mesh channel.
- **News File Access**: Retrieve the contents of a news file on request; supports multiple news sources or files.
- **Shell Command Access**: Execute shell commands via DM with replay protection (admin only).
### 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!
- **HTML Reports**: Visualize bot traffic and data flows with a built-in HTML generator. See [data reporting](logs/README.md) for details.
- **RSS & News Feeds**: Receive news and data from multiple sources directly on the mesh.
### Robust Message Handling
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
- **Automatic Message Chunking**: Messages over 160 characters are automatically split to ensure reliable delivery across multiple hops.
## Getting Started
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project. 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, armbian or [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project.
🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
### Quick Setup
#### Clone the Repository
@@ -92,450 +107,26 @@ If you dont have git you will need it `sudo apt-get install git`
```sh
git clone https://github.com/spudgunman/meshing-around
```
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
## Full list of commands for the bot
### Networking
| Command | Description | ✅ Works Off-Grid |
|---------|-------------|-
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `leaderboard` | Shows extreme mesh metrics like lowest battery 🪫 `leaderboard reset` allows admin reset | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `sysinfo` | Returns the bot node telemetry info | ✅ |
| `test` | used to test the limits of data transfer (`test 4` sends data to the maxBuffer limit default 200 charcters) via DM only | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `echo` | Echo string back, disabled by default | ✅ |
### Radio Propagation & Weather Forecasting
| Command | Description | |
|---------|-------------|-------------------
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
| `earthquake` | Returns the largest and number of USGS events for the location | |
| `hfcond` | Returns a table of HF solar conditions | |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `riverflow` | Return information from NOAA for river flow info. | |
| `solar` | Gives an idea of the x-ray flux | |
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `tide` | Returns the local tides (NOAA data source) | |
| `valert` | Returns USGS Volcano Data | |
| `wx` | Return local weather forecast, NOAA or Open Meteo (which also has `wxc` for metric and imperial) | |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
| `mwx` | Return the NOAA Coastal Marine Forecast data | |
### Bulletin Board & Mail
| Command | Description | |
|---------|-------------|-
| `bbshelp` | Returns the following help message | ✅ |
| `bbslist` | Lists the messages by ID and subject | ✅ |
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
| `sms:` | Send sms-email to multiple address on file | |
| `setemail`| Sets the email for easy communications | |
| `setsms` | Adds the SMS-Email for quick communications | |
| `clearsms` | Clears all SMS-Emails on file for node | |
### Data Lookup
| Command | Description | |
|---------|-------------|-
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard on device, like Store and Forward, returns the PublicChannel and Current | ✅ |
| `readnews` | returns the contents of a file (data/news.txt, by default) can also `news mesh` via the chunker on air | ✅ |
| `readrss` | returns a set RSS feed on air | |
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
| `wiki:` | Searches Wikipedia (or local Kiwix server) and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `howfar` | returns the distance you have traveled since your last HowFar. `howfar reset` to start over | ✅ |
| `howtall` | returns height of something you give a shadow by using sun angle | ✅ |
### CheckList
| Command | Description | |
|---------|-------------|-
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
| `checklist` | Display the checklist database, with note | ✅ |
### Games (via DM only)
| Command | Description | |
|---------|-------------|-
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
| `hangman` | Plays the classic word guess game | ✅ |
| `joke` | Tells a joke | |
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `survey` | Issues out a survey to the user | ✅ |
| `quiz` | QuizMaster Bot `q: ?` for more | ✅ |
| `tic-tac-toe`| Plays the game classic game | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
#### QuizMaster
To use QuizMaster the bbs_admin_list is the QuizMaster, who can `q: start` and `q: stop` to start and stop the game, `q: broadcast <message>` to send a message to all players.
Players can `q: join` to join the game, `q: leave` to leave the game, `q: score` to see their score, and `q: top` to see the top 3 players.
To Answer a question, just type the answer prefixed with `q: <answer>`
#### Survey
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
## Other Install Options
- **Automated Installation**: [install.sh](INSTALL.md) will automate optional venv and requirements installation.
- **Launch Script**: [laynch.sh](INSTALL.md) only used in a venv install, to launch the bot and the report generator.
### Docker Installation - handy for windows
See further info on the [docker.md](script/docker/README.md)
### Manual Install
Install the required dependencies using pip:
```sh
pip install -r requirements.txt
```
## Full list of commands for the bot
[modules/README.md](modules/README.md)
Copy the configuration template to `config.ini` and edit it to suit your needs:
```sh
cp config.template config.ini
```
### Configuration Guide
The following is documentation for the config.ini file
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
```sh
meshtastic --ble-scan
```
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
```ini
# 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
# mac is the MAC address of the device to connect to for BLE type
[interface]
type = serial
# port = '/dev/ttyUSB0'
# hostname = 192.168.0.1
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support. See config.template for more.
[interface2]
enabled = False
```
### General Settings
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
```ini
[general]
respond_by_dm_only = True
defaultChannel = 0
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
cmdBang = False # require ! to be the first character in a command
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
```
### Location Settings
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
```ini
[location]
enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
coastalForecastDays = 3 # number of data points to return, default is 3
```
### Module Settings
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
```ini
[bbs]
enabled = False
[general]
DadJokes = False
StoreForward = False
```
### History
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
```ini
enableCmdHistory = True # history command enabler
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
```
### Sentry Settings
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
```ini
SentryEnabled = True # detect anyone close to the bot
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
highFlyingAlert = True # HighFlying Node alert
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
```
### E-Mail / SMS Settings
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
```ini
[smtp]
# enable or disable the SMTP module, minimum required for outbound notifications
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
sysopEmails =
# See config.template for all the SMTP settings
SMTP_SERVER = smtp.gmail.com
SMTP_AUTH = True
EMAIL_SUBJECT = Meshtastic✉
```
### Emergency Response Handler
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
```ini
[emergencyHandler]
enabled = True # enable or disable the emergency response handler
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
alert_interface = 1
```
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS and NINA
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
```ini
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
ignoreFEMAenable = True # Ignore any headline that includes followig word list
ignoreFEMAwords = test,exercise
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# To use other country services enable only a single optional serivce
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
myRegionalKeysDE = 110000000000,120510000000
```
#### NOAA EAS
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
```ini
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
ignoreEASenable = True # Ignore any headline that includes followig word list
ignoreEASwords = test,advisory
```
#### USGS River flow data and Volcano alerts
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
Volcano Alerts use lat/long to determine ~1000km radius
```ini
[location]
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
riverList = 14144700 # example Mouth of Columbia River
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
```
### Repeater Settings
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
```ini
[repeater] # repeater module
enabled = True
repeater_channels = [2, 3]
```
### Ollama (LLM/AI) Settings
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
```ini
# Enable ollama LLM see more at https://ollama.com
ollama = True # Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
```
Also see `llm.py` for changing the defaults of:
```ini
# LLM System Variables
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
# Used in the meshBotAI template (legacy)
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
### Wikipedia Search Settings
The Wikipedia search module can use either the online Wikipedia API or a local Kiwix server for offline wiki access. Kiwix is especially useful for mesh networks operating in remote or offline environments.
```ini
# Enable or disable the wikipedia search module
wikipedia = True
# Use local Kiwix server instead of online Wikipedia
# Set to False to use online Wikipedia (default)
useKiwixServer = False
# Kiwix server URL (only used if useKiwixServer is True)
kiwixURL = http://127.0.0.1:8080
# Kiwix library name (e.g., wikipedia_en_100_nopic_2024-06)
# Find available libraries at https://library.kiwix.org/
kiwixLibraryName = wikipedia_en_100_nopic_2024-06
```
To set up a local Kiwix server:
1. Install Kiwix tools: https://kiwix.org/en/ `sudo apt install kiwix-tools -y`
2. Download a Wikipedia ZIM file to `data/`: https://library.kiwix.org/ `wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_nopic_2025-09.zim`
3. Run the server: `kiwix-serve --port 8080 wikipedia_en_100_nopic_2025-09.zim`
4. Set `useKiwixServer = True` in your config.ini
The bot will automatically extract and truncate content to fit Meshtastic's message size limits (~500 characters).
### Radio Monitoring
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```ini
[radioMon]
enabled = True
rigControlServerAddress = localhost:4532
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
signalHoldTime = 10 # hold time for high SNR
signalCooldown = 5 # the following are combined to reset the monitor
signalCycleLimit = 5
```
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
filemon_enabled = True
file_path = alert.txt # text file to monitor for changes
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
enable_read_news = False # news command will return the contents of a text file
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
allowXcmd = True
```
#### Offline EAS
To Monitor EAS with no internet connection see the following notes
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
no examples yet for these tools
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [dsame3](https://github.com/jamieden/dsame3)
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
```bash
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
```
The following example shell command will pipe rtl_sdr to alert.txt
```bash
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
#### Newspaper on mesh
Maintain multiple news sources. Each source should be a file named `{source}_news.txt` in the `data/` directory (for example, `data/mesh_news.txt`).
- To read the default news, use the `readnews` command (reads from `data/news.txt`.
- To read a specific source, use `readnews abc` to read from `data/abc_news.txt`.
This allows you to organize and access different news feeds or categories easily.
External scripts can update these files as needed, and the bot will serve the latest content on request.
### Greet new nodes QRZ module
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
```ini
[qrz]
enabled = True # QRZ Hello to new nodes
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
training = True # Training mode will not send the hello message to new nodes, use this to build up database
```
### Scheduler
In the config.ini enable the module
```ini
[scheduler]
enabled = False # enable or disable the scheduler module
interface = 1 # channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
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.
```python
#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 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))
```
#### 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.
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
```
```ini
bbslink_enabled = True
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
```
### Games (via DM only)
[modules/games/README.md](modules/games/README.md)
### Firmware 2.6 DM Key, and 2.7 CLIENT_BASE Favorite Nodes
Firmware 2.6 introduced [PKC](https://meshtastic.org/blog/introducing-new-public-key-cryptography-in-v2_5/), enabling secure private messaging by adding necessary keys to each node. To fully utilize this feature, you should add favorite nodes—such as BBS admins—to your nodes favorites list to ensure their keys are retained. A helper script is provided to simplify this process:
- Run the helper script from the main program directory: `python3 script/addFav.py`
- By default, this script adds nodes from `bbs_admin_list` and `bbslink_whitelist`
- If using a virtual environment, run: `launch.sh addfav`
- The API will not work-fully today to set nodes this is a WIP
Additionally, you can just DM a bot to "auto favorite." If your node is set to not be messageable, DMs won't work—be advised.
To configure favorite nodes, add their numbers to your config file:
```conf
@@ -579,43 +170,11 @@ 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, snydermesh, trs2982, FJRPilot, Josh, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **FJRPiolt**: testing bugs out!!
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, Malice, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
### Requirements
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install schedule
pip install wikipedia
```
For the Ollama LLM:
```sh
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| git pull| :white_check_mark: |
## Reporting a Vulnerability
If its serious, its likley big. otherwise post issues, reachout on discord.

43
compose.yaml Normal file
View File

@@ -0,0 +1,43 @@
# Docker Compose configuration for Meshing Around.
# This setup includes the main Meshing Around service, with optional Ollama and Prometheus Node Exporter services.
# Adjust device mappings, ports, and configurations as needed for your environment.
services:
meshing-around:
stdin_open: true
tty: true
ports:
- 8420:8420
devices: # Optional if using meshtasticd. Pass through radio device.
- /dev/ttyUSB0 # Replace this with your actual device!
#- /dev/ttyAMA0 # Example
volumes:
- /data/meshing-around/config.ini:/app/config.ini:rw
image: ghcr.io/SpudGunMan/meshing-around:test-all-changes
container_name: meshing-around
restart: unless-stopped
environment:
- OLLAMA_API_URL=http://ollama:11434
extra_hosts:
#- "host.docker.internal:host-gateway" # Enables access to host services from within the container.
user: "1000:1000" # run as non-root user for better security
meshtasticd: # Runs a virtual node. Optional, but can be used to link meshing-around directly to mqtt.
ports:
- 4403:4403
restart: unless-stopped
container_name: meshtasticd
image: meshtastic/meshtasticd:beta
ollama: # Used for enabling LLM interactions.
ports:
- 11434:11434 # Ollama API port
volumes:
- /data/ollama:/root/.ollama
container_name: ollama
image: ollama/ollama:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 5

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]
@@ -127,18 +127,28 @@ alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
# device interface and channel to send the alert message to
SentryInterface = 1
SentryChannel = 2
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
emailSentryAlerts = False
# Enable detection sensor alert, requires external GPIO sensor connected to node
detectionSensorAlert = False
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# Enable detection sensor alert, requires external sensor connected to node
detectionSensorAlert = False
# list of watched nodes numbers ex: 2813308004,4258675309
sentryWatchList =
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# Enable running external shell command when sentry alert is triggered
cmdShellSentryAlerts = False
# External shell command to run when sentry alert is triggered
sentryAlertNear = sentry_alert_near.sh
sentryAlertAway = sentry_alert_away.sh
# HighFlying Node alert
highFlyingAlert = True
@@ -171,6 +181,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
@@ -236,7 +248,7 @@ enableDEalerts = False
myRegionalKeysDE = 110000000000,120510000000
# Satalite Pass Prediction
# Register for free API https://www.n2yo.com/login/
# Register for free API https://www.n2yo.com/login/ personal data page at bottom 'Are you developer?'
n2yoAPIKey =
# NORAD list https://www.n2yo.com/satellites/
satList = 25544,7530
@@ -274,7 +286,9 @@ channel = 2
message = "MeshBot says Hello! DM for more info."
# enable overides the above and uses the motd as the message
schedulerMotd = False
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
# value can also be joke (everyXmin) or weather (hour) or link (hour) for special auto messages
# custom for module/scheduler.py custom schedule examples
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
@@ -300,6 +314,13 @@ signalCycleLimit = 5
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
@@ -364,12 +385,15 @@ golfsim = True
hangman = True
hamtest = True
tictactoe = True
wordOfTheDay = 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
# 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
@@ -384,7 +408,7 @@ splitDelay = 2.5
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

75
etc/README.md Normal file
View File

@@ -0,0 +1,75 @@
# etc Directory
This folder contains supporting files and resources for the Mesh Bot project. Typical contents include:
- **Images**: Visual assets used in documentation (e.g., `pong-bot.jpg`).
- **Custom Scripts**: Example or utility scripts for advanced configuration (e.g., `custom_scheduler.py` for scheduled tasks).
- **tmp**: Temp files for install
## db_admin.py
**Purpose:**
`db_admin.py` is a simple administrative tool for viewing the contents of the Mesh Bots data and high score databases. It loads and prints out messages, direct messages, email/SMS records, and game high score tables stored in the `/data` directory.
**Usage:**
Run this script from the command line to display the current contents of the bots databases. This is useful for debugging, verifying data integrity, or reviewing stored messages and game scores.
```sh
python etc/db_admin.py
```
**What it does:**
- Attempts to load various `.pkl` and `.pickle` files from the `data` directory.
- Prints out the contents of BBS messages, direct messages, email and SMS databases.
- Displays high scores for supported games (Lemonade Stand, DopeWars, BlackJack, Video Poker, Mastermind, GolfSim).
- If a file is missing, it will print a message indicating so.
**Note:**
This tool is for administrative and debugging purposes only. It does not modify any data.
## eas_alert_parser.py
**Purpose:**
`eas_alert_parser.py` is a utility script for processing and cleaning up output from `multimon-ng` to extract and convert Emergency Alert System (EAS) messages for further use, such as with EAS2Text.
**Usage:**
This script is intended to be used with piped input, typically from `multimon-ng` decoding SAME/EAS messages. It filters and processes EAS lines, converts them to readable text using EAS2Text, and writes the results to `alert.txt`.
**Example usage:**
```sh
multimon-ng -a EAS ... | python etc/eas_alert_parser.py
```
**What it does:**
- Reads input line-by-line (supports piped or redirected input).
- Filters for lines starting with `EAS:` or `EAS (part):`.
- Avoids duplicate messages and only processes new alerts.
- Uses the EAS2Text library to convert EAS codes to human-readable messages.
- Writes completed alerts to `alert.txt` for further processing or notification.
**Note:**
This script is intended for experimental or hobbyist use and may require customization for your specific workflow.
## simulator.py
**Purpose:**
`simulator.py` is a development and testing tool that simulates the behavior of the Mesh Bot in a controlled environment. It allows you to prototype and test handler functions without needing real hardware or a live mesh network.
**Usage:**
Run this script from the command line to interactively test handler functions. You can input messages as if you were a mesh node, and see how your handler responds.
```sh
python etc/simulator.py
```
**What it does:**
- Simulates node IDs, device IDs, and random GPS locations.
- Lets you specify which handler function to test (by setting `projectName`).
- Prompts for user input, passes it to the handler, and displays the response.
- Logs simulated message sending and handler output for review.
- Useful for rapid prototyping and debugging new features or message handlers.
**Note:**
Edit the `projectName` variable to match the handler function you want to test. You can expand this script to test additional handlers or scenarios as needed.
Feel free to add or update resources here as needed for documentation, configuration, or project support.

52
etc/custom_scheduler.py Normal file
View File

@@ -0,0 +1,52 @@
import schedule
from modules.log import logger
from modules.system import send_message
def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc, MOTD, schedulerChannel, schedulerInterface):
# custom scheduler job to run the schedule see examples below
logger.debug(f"System: Starting the custom_scheduler.py default to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
# Enhanced Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Send a joke every 2 minutes
#logger.debug(f"System: Custom Scheduler: Send a joke every 2 minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# 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
#logger.debug("System: Custom Scheduler: Send WX every Morning at 08:00")
#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
#logger.debug("System: Custom Scheduler: Config URL for Medium Fast Network Use every other day at 10:00")
#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
#logger.debug(f"System: Custom Scheduler: Welcome Notice for group on the 15th and 25th of the month at 12:00 on Device:{schedulerInterface} Channel:{schedulerChannel}")
#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 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
#logger.debug("System: Custom Scheduler: bbslink MeshBot 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

@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 mesh_bot.py
ExecStop=pkill -f mesh_bot.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

View File

@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 modules/web.py
ExecStop=pkill -f mesh_bot_w3.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
@@ -21,3 +23,6 @@ Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems
[Install]
WantedBy=default.target

View File

@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 pong_bot.py
ExecStop=pkill -f pong_bot.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

View File

@@ -13,13 +13,18 @@ printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded syste
printf "If there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# fuse check for existing installation
if [[ -f config.ini ]]; then
printf "\nDetected existing installation, please backup and remove existing installation before proceeding\n"
exit 1
fi
# check if we are in /opt/meshing-around
if [ $program_path != "/opt/meshing-around" ]; then
if [[ "$program_path" != "/opt/meshing-around" ]]; then
printf "\nIt is suggested to project path to /opt/meshing-around\n"
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
read move
if [[ $(echo "$move" | grep -i "^y") ]]; then
sudo mv $program_path /opt/meshing-around
sudo mv "$program_path" /opt/meshing-around
cd /opt/meshing-around
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
exit 0
@@ -73,9 +78,9 @@ fi
# add user to groups for serial access
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
sudo usermod -a -G bluetooth $USER
sudo usermod -a -G dialout "$USER"
sudo usermod -a -G tty "$USER"
sudo usermod -a -G bluetooth "$USER"
# copy service files
cp etc/pong_bot.tmp etc/pong_bot.service
@@ -83,6 +88,12 @@ cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
# copy modules/custom_scheduler.py template if it does not exist
if [[ ! -f modules/custom_scheduler.py ]]; then
cp etc/custom_scheduler.py modules/custom_scheduler.py
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
fi
# generate config file, check if it exists
if [[ -f config.ini ]]; then
printf "\nConfig file already exists, moving to backup config.old\n"
@@ -175,10 +186,10 @@ fi
# set the correct path in the service file
replace="s|/dir/|$program_path/|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
# set the correct user in the service file?
#ask if we should add a user for the bot
@@ -198,26 +209,33 @@ else
whoami=$(whoami)
fi
# set basic permissions for the bot user
sudo usermod -a -G dialout $whoami
sudo usermod -a -G tty $whoami
sudo usermod -a -G bluetooth $whoami
sudo usermod -a -G dialout "$whoami"
sudo usermod -a -G tty "$whoami"
sudo usermod -a -G bluetooth "$whoami"
echo "Added user $whoami to dialout, tty, and bluetooth groups"
sudo chown -R $whoami:$whoami $program_path/logs
sudo chown -R $whoami:$whoami $program_path/data
sudo chown -R "$whoami:$whoami" "$program_path/logs"
sudo chown -R "$whoami:$whoami" "$program_path/data"
echo "Permissions set for meshbot on logs and data directories"
# check and see if some sort of NTP is running
if ! systemctl is-active --quiet ntp.service && \
! systemctl is-active --quiet systemd-timesyncd.service && \
! systemctl is-active --quiet chronyd.service; then
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
fi
# set the correct user in the service file
replace="s|User=pi|User=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
printf "\n service files updated\n"
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
@@ -287,7 +305,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
@@ -299,6 +317,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
@@ -336,7 +355,7 @@ else
else
printf "\nCron job already exists, skipping\n"
fi
printf "Reference following commands:\n\n" "$service" > install_notes.txt
printf "Reference following commands:\n\n" > install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
@@ -344,9 +363,10 @@ else
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
fi
printf "\nInstallation complete!\n"
printf "\nInstallation complete?\n"
exit 0

View File

@@ -5,12 +5,12 @@
cd "$(dirname "$0")"
if [ ! -f "config.ini" ]; then
if [[ ! -f "config.ini" ]]; then
cp config.template config.ini
fi
# activate the virtual environment if it exists
if [ -d "venv" ]; then
if [[ -d "venv" ]]; then
source venv/bin/activate
else
echo "Virtual environment not found, this tool just launches the .py in venv"
@@ -22,9 +22,9 @@ if [[ "$1" == pong* ]]; then
python3 pong_bot.py
elif [[ "$1" == mesh* ]]; then
python3 mesh_bot.py
elif [ "$1" == "html" ]; then
elif [[ "$1" == "html" ]]; then
python3 etc/report_generator.py
elif [ "$1" == "html5" ]; then
elif [[ "$1" == "html5" ]]; then
python3 etc/report_generator5.py
elif [[ "$1" == add* ]]; then
python3 script/addFav.py

View File

@@ -1,10 +1,16 @@
# Logs and Reports
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
## Reporting Note
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
- If you are in a venv and using launch.sh you can `launch.sh html5`
This directory stores log files generated by the Mesh Bot. To generate useful reports, ensure you have at least a day's worth of logs or a substantial number of messages.
## Reporting
Reports are generated using [`../etc/report_generator5.py`](../etc/report_generator5.py), which produces modern HTML5 reports. The output (`index.html`) is saved in [`../etc/www`](../etc/www) by default. A `.cfg` configuration file is created on first run, allowing you to customize settings such as the web root directory.
- Ensure `SyslogToFile = True` and `sysloglevel = DEBUG` in your configuration to enable full reporting.
- If using a virtual environment and `launch.sh`, you can run:
```sh
launch.sh html5
```
![reportView](../etc/reporting.jpg)

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,878 @@
# Modules and Adding stuff
# Meshtastic Mesh-Bot Modules
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
This document provides an overview of all modules available in the Mesh-Bot project, including their features, usage, and configuration.
Updated Oct-2025 "ver 1.9.8.4"
## By following these steps, you can add a new bbs option to the bot.
---
1. **Define the Command Handler**:
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
```python
def handle_newcommand(message, message_from_id, deviceID):
return "This is a response from the new command."
```
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
2. **Add the Command to the Auto Response**:
Update the auto_response function in mesh_bot.py to include the new command:
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
#...
```
3. **Update the Trap List and Help**:
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
```python
#...
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
help_message = "Bot CMD?:newcommand, "
#...
```
## Table of Contents
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
```python
# newcommand Configuration
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
if newcommand_enabled:
trap_list_newcommand = ("newcommand",)
trap_list = trap_list + trap_list_newcommand
help_message = help_message + ", newcommand"
```
- [Overview](#overview)
- [Networking](#networking)
- [Games](#games)
- [BBS (Bulletin Board System)](#bbs-bulletin-board-system)
- [Checklist](#checklist)
- [Location & Weather](#location--weather)
- [EAS & Emergency Alerts](#eas--emergency-alerts)
- [File Monitoring & News](#file-monitoring--news)
- [Radio Monitoring](#radio-monitoring)
- [Voice Commands (VOX)](#voice-commands-vox)
- [Ollama LLM/AI](#ollama-llmai)
- [Wikipedia Search](#wikipedia-search)
- [Scheduler](#-mesh-bot-scheduler-user-guide)
- [Other Utilities](#other-utilities)
- [Configuration](#configuration)
- [Troubleshooting](#troubleshooting)
- [Adding your Own](adding_more.md)
- [Configuration Guide](#configuration-guide)
5. **Test the New Command**:
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
---
## Overview
Modules are Python files in the `modules/` directory that add features to the bot. Enable or disable them via `config.ini`.
See [modules/adding_more.md](adding_more.md) for developer notes.
---
## Networking
### ping / pinging / test / testing / ack
- **Usage:** `ping`, `pinging`, `test`, `testing`, `ack`, `ping @user`, `ping #tag`
- **Description:** Sends a ping to the bot. The bot responds with signal information such as SNR (Signal-to-Noise Ratio), RSSI (Received Signal Strength Indicator), and hop count. Used for making field report etc.
- **Targeted Ping:**
You can direct a ping to a specific user or group by mentioning their short name or tag:
- `ping @NODE` — Pings a Joke to specific node by its short name.
- **Example:**
```
ping
```
Response:
```
SNR: 12.5, RSSI: -80, Hops: 2
```
```
ping @Top of the hill
```
Response:
```
PING @Top of the hill SNR: 10.2, RSSI: -85, Hops: 1
```
- **Help:**
Send `ping?` in a Direct Message (DM) for usage instructions.
---
### Notes
- You can mention users or tags in your ping/test messages (e.g., `ping @user` or `ping #group`) to target specific nodes or groups.
- Some commands may only be available in Direct Messages, depending on configuration.
| Command | Description | ✅ Works Off-Grid |
|--------------|-------------|------------------|
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) you can also ping @NODE short name and if BBS DM enabled it will send them a joke | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `leaderboard` | Shows extreme mesh metrics like lowest battery 🪫 `leaderboard reset` allows admin reset | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `sysinfo` | Returns the bot node telemetry info | ✅ |
| `test` | used to test the limits of data transfer (`test 4` sends data to the maxBuffer limit default 200 charcters) via DM only | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `echo` | Echo string back, disabled by default | ✅ |
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
---
## Games
All games are played via DM to the bot. See [modules/games/README.md](games/README.md) for detailed rules and examples.
| Command | Description |
|----------------|------------------------------------|
| `blackjack` | Play Blackjack (Casino 21) |
| `dopewars` | Classic trading game |
| `golfsim` | 9-hole Golf Simulator |
| `lemonstand` | Lemonade Stand business sim |
| `tictactoe` | Tic-Tac-Toe vs. the bot |
| `mastermind` | Code-breaking game |
| `videopoker` | Video Poker (five-card draw) |
| `joke` | Tells a dad joke |
| `hamtest` | FCC/ARRL QuizBot |
| `hangman` | Classic word guess game |
| `survey` | Take a custom survey |
| `quiz` | QuizMaster group quiz |
Enable/disable games in `[games]` section of `config.ini`.
---
## BBS (Bulletin Board System)
| Command | Description |
|--------------|-----------------------------------------------|
| `bbshelp` | Show BBS help |
| `bbslist` | List messages |
| `bbsread` | Read a message by ID |
| `bbspost` | Post a message or DM |
| `bbsdelete` | Delete a message |
| `bbsinfo` | BBS stats (sysop) |
| `bbslink` | Link messages between BBS systems |
Enable in `[bbs]` section of `config.ini`.
more at [meshBBS: How-To & API Documentation](bbstools.md)
---
## Checklist
| Command | Description |
|--------------|-----------------------------------------------|
| `checkin` | Check in a node/asset |
| `checkout` | Check out a node/asset |
| `checklist` | Show checklist database |
Enable in `[checklist]` section of `config.ini`.
---
## Location & Weather
| Command | Description |
|--------------|-----------------------------------------------|
| `wx` | Local weather forecast (NOAA/Open-Meteo) |
| `wxc` | Weather in metric/imperial |
| `wxa` | NOAA alerts |
| `wxalert` | NOAA alerts (expanded) |
| `mwx` | NOAA Coastal Marine Forecast |
| `tide` | NOAA tide info |
| `riverflow` | NOAA river flow info |
| `earthquake` | USGS earthquake info |
| `valert` | USGS volcano alerts |
| `rlist` | Nearby repeaters from RepeaterBook |
| `satpass` | Satellite pass info |
| `howfar` | Distance traveled since last check |
| `howtall` | Calculate height using sun angle |
| `whereami` | Show current location |
Configure in `[location]` section of `config.ini`.
Certainly! Heres a README help section for your `mapHandler` command, suitable for users of your meshbot:
---
## 📍 Map Command
The `map` command allows you to log your current GPS location with a custom description. This is useful for mapping mesh nodes, events, or points of interest.
### Usage
- **Show Help**
```
map help
```
Displays usage instructions for the map command.
- **Log a Location**
```
map <description>
```
Example:
```
map Found a new mesh node near the park
```
This will log your current location with the description "Found a new mesh node near the park".
### How It Works
- The bot records your user ID, latitude, longitude, and your description in a CSV file (`data/map_data.csv`).
- If your location data is missing or invalid, youll receive an error message.
- You can view or process the CSV file later for mapping or analysis.
**Tip:** Use `map help` at any time to see these instructions in the bot.
---
## EAS & Emergency Alerts
| Command | Description |
|--------------|-----------------------------------------------|
| `ea`/`ealert`| FEMA iPAWS/EAS alerts (USA/DE) |
Enable in `[eas]` section of `config.ini`.
---
## File Monitoring & News
| Command | Description |
|--------------|-----------------------------------------------|
| `readnews` | Read contents of a news file |
| `readrss` | Read RSS feed |
| `x:` | Run shell command (if enabled) |
Configure in `[fileMon]` section of `config.ini`.
---
## Radio Monitoring
| Command | Description |
|--------------|-----------------------------------------------|
| `radio` | Monitor radio SNR via Hamlib |
Configure in `[radioMon]` section of `config.ini`.
---
## Voice Commands (VOX)
You can trigger select bot functions using voice commands with the "Hey Chirpy!" wake word.
Just say "Hey Chirpy..." followed by one of the supported commands:
| Voice Command | Description |
|---------------|---------------------------------------------|
| `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 |
Enable and configure VOX features in the `[vox]` section of `config.ini`.
---
## Ollama LLM/AI
| Command | Description |
|--------------|-----------------------------------------------|
| `askai` | Ask Ollama LLM AI |
| `ask:` | Ask Ollama LLM AI (raw) |
Configure in `[ollama]` section of `config.ini`.
---
## Wikipedia Search
| Command | Description |
|--------------|-----------------------------------------------|
| `wiki:` | Search Wikipedia or local Kiwix server |
Configure in `[wikipedia]` section of `config.ini`.
---
## 📅 Mesh Bot Scheduler User Guide
Automate messages and tasks using the scheduler module.
Configure in `[scheduler]` section of `config.ini`.
See modules/custom_scheduler.py for advanced scheduling using python
**Purpose:**
`scheduler.py` provides automated scheduling for Mesh Bot, allowing you to send messages, jokes, weather updates, and custom actions at specific times or intervals.
**How to Use:**
- The scheduler is configured via your bots settings or commands, specifying what to send, when, and on which channel/interface.
- Supports daily, weekly, hourly, and minutely schedules, as well as special jobs like jokes and weather.
- For advanced automation, you can define your own schedules in `etc/custom_scheduler.py` (copied to `modules/custom_scheduler.py` at install).
**Features:**
- **Basic Scheduling:** Send messages on a set schedule (e.g., every day at 09:00, every Monday at noon, every hour, etc.).
- **Joke Scheduler:** Automatically send jokes at a chosen interval.
- **Weather Scheduler:** Send weather updates at a chosen interval.
- **Custom Scheduler:** Import and run your own scheduled jobs by editing `custom_scheduler.py`.
- **Logging:** All scheduling actions are logged for debugging and monitoring.
**Example Configuration:**
To send a daily message at 09:00:
- `schedulerValue = 'day'`
- `schedulerTime = '09:00'`
- `schedulerMessage = 'Good morning, mesh!'`
**Custom Schedules:**
1. Edit `etc/custom_scheduler.py` to define your custom jobs.
2. On install, this file is copied to `modules/custom_scheduler.py`.
3. Set `schedulerValue = 'custom'` to activate your custom schedules.
**Note:**
- The scheduler uses the [schedule](https://schedule.readthedocs.io/en/stable/) Python library.
- All scheduled jobs run asynchronously as long as the bot is running.
- For troubleshooting, check the logs for scheduler activity and errors.
### Running a Shell command
### Basic Scheduler Options
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
You can schedule messages or actions using the following options in your configuration:
- **day**: Run every day at a specific time or every N days.
- **mon, tue, wed, thu, fri, sat, sun**: Run on a specific day of the week at a specific time.
- **hour**: Run every N hours.
- **min**: Run every N minutes.
#### **Examples:**
| Option | Time/Interval | What it does |
|-------------|--------------|---------------------------------------------------|
| `day` | `08:00` | Runs every day at 8:00 AM |
| `day` | `2` | Runs every 2 days |
| `mon` | `09:30` | Runs every Monday at 9:30 AM |
| `hour` | `1` | Runs every hour |
| `min` | `30` | Runs every 30 minutes |
- If you specify a day (e.g., `mon`) and a time (e.g., `09:30`), the message will be sent at that time on that day.
- If you specify `hour` or `min`, set the interval (e.g., every 2 hours or every 15 minutes).
---
### Special Scheduler Options
#### **joke**
- Schedules the bot to send a random joke at the specified interval.
- **Example:**
- Option: `joke`
- Interval: `60`
- → Sends a joke every 60 minutes.
#### **link**
- Schedules the bot to send a satellite link message at the specified interval (in hours).
- **Example:**
- Option: `link`
- Interval: `2`
- → Sends a bbslink message every 2 hours.
#### **weather**
- Schedules the bot to send a weather update at the specified interval (in hours).
- **Example:**
- Option: `weather`
- Interval: `3`
- → Sends a weather update every 3 hours.
---
### Days of the Week
You can use any of these options to schedule messages on specific days:
- `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`
**Example:**
- Option: `fri`
- Time: `17:00`
- → Sends the message every Friday at 5:00 PM.
---
### Configuration Fields
- **schedulerValue**: The schedule type (e.g., `day`, `joke`, `weather`, `mon`, etc.)
- **schedulerTime**: The time to run (e.g., `08:00`). Leave blank for interval-based schedules.
- **schedulerInterval**: The interval (e.g., `2` for every 2 hours/days/minutes).
- **schedulerChannel**: The channel number to send to.
- **schedulerInterface**: The device/interface number.
---
## Other Utilities
- `motd` — Message of the day
- `leaderboard` — Mesh telemetry stats
- `lheard` — Last heard nodes
- `history` — Command history
- `cmd`/`cmd?` — Show help message (the bot avoids the use of saying or using help)
---
## Configuration
- Edit `config.ini` to enable/disable modules and set options.
- See `config.template` for all available settings.
- Each module section in `config.ini` has an `enabled` flag.
---
## Troubleshooting
- Use the `logger` module for debug output.
- See [modules/README.md](modules/README.md) for developer help.
- Use `etc/simulator.py` for local testing.
- Check the logs in the `logs/` directory for errors.
### .ini Settings
If you encounter issues with modules or bot behavior, you can use the `.ini` configuration settings to help diagnose and resolve problems:
### Enable Debug Logging
Increase the logging level to capture more detailed information:
```ini
[general]
sysloglevel = DEBUG
SyslogToFile = True
```
This will log detailed system messages to disk, which you can review in the `logs/` directory.
### Module-Specific Troubleshooting
- **Games Not Working:**
Ensure the relevant game is enabled in the `[games]` section:
```ini
[games]
blackjack = True
dopeWars = True
# ...other games
```
- **Weather/Location Issues:**
Make sure `[location]` and weather modules are enabled and configured:
```ini
[location]
enabled = True
lat = 48.50
lon = -123.0
```
- **BBS Not Responding:**
Check that BBS is enabled and you are not on the ban list:
```ini
[bbs]
enabled = True
bbs_ban_list =
```
- **Scheduler Not Running:**
Confirm the scheduler is enabled and properly configured:
```ini
[scheduler]
enabled = True
value = day
time = 09:00
```
- **File Monitoring Not Working:**
Verify file monitoring is enabled and the correct file path is set:
```ini
[fileMon]
filemon_enabled = True
file_path = alert.txt
```
---
## Messaging Settings
The `[messagingSettings]` section in your `config.ini` controls how messages are handled, split, acknowledged, and logged by Mesh Bot. Adjust these settings to optimize performance, reliability, and debugging for your mesh network.
### Key Options
- **responseDelay**
*Default: 2.2*
Sets the delay (in seconds) before the bot responds to a message. Increase this if you experience message collisions or throttling on busy networks.
- **splitDelay**
*Default: 2.5*
Sets the delay (in seconds) between sending split message chunks. Useful for avoiding collisions when sending long messages that are broken into parts.
- **MESSAGE_CHUNK_SIZE**
*Default: 160*
The maximum number of characters per message chunk. Messages longer than this are automatically split. (The chunker may allow up to 3 extra characters.)
- **wantAck**
*Default: False*
If set to `True`, the bot will request over-the-air (OTA) acknowledgements for sent messages. Enable this for critical messages, but note it may increase network traffic.
- **maxBuffer**
*Default: 200*
Sets the maximum buffer size (in bytes) for radio testing. Increase or decrease based on your hardwares capabilities.
- **enableHopLogs**
*Default: False*
If `True`, enables extra logging of hop count data for each message. Useful for analyzing mesh performance.
- **noisyNodeLogging**
*Default: False*
Enables logging for nodes that generate excessive telemetry or noise. Helps identify problematic devices.
- **noisyTelemetryLimit**
*Default: 5*
The threshold for how many noisy packets trigger logging for a node.
- **logMetaStats**
*Default: True*
Enables logging of metadata statistics for analysis.
- **DEBUGpacket**
*Default: False*
If `True`, logs all packet details for advanced debugging. Only enable if you need deep diagnostics, as this can generate large log files.
- **debugMetadata**
*Default: False*
Enables detailed logging for metaPackets. Use the `metadataFilter` to control which packet types are logged.
- **metadataFilter**
*Default: TELEMETRY_APP,POSITION_APP*
Comma-separated list of packet types to include in metaPacket logging. Adjust to focus on specific data types.
### Troubleshooting Tips
- If you see message collisions or dropped messages, try increasing `responseDelay` and `splitDelay`.
- Enable `DEBUGpacket` and `enableHopLogs` for detailed diagnostics if youre troubleshooting delivery or routing issues.
- Use `noisyNodeLogging` to identify and address problematic nodes on your mesh.
---
**Tip:**
Refer to the comments in `config.template` for further guidance on each setting.
### General Tips
- After changing `.ini` settings, restart the bot to apply changes.
- Check the logs in the `logs/` directory for errors or warnings.
- Use `explicitCmd = True` in `[general]` to require explicit commands, which can help avoid accidental triggers.
- For advanced debugging, set `DEBUGpacket = True` in `[messagingSettings]` to log all packet details.
---
If you continue to have issues, review the logs for error messages and consult the comments in `config.template` for further guidance.
---
### Configuration Guide
The following is documentation for the config.ini file
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
```sh
meshtastic --ble-scan
```
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
```ini
# 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/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for BLE type
[interface]
type = serial
# port = '/dev/ttyUSB0'
# hostname = 192.168.0.1
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support. See config.template for more.
[interface2]
enabled = False
```
### General Settings
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
```ini
[general]
respond_by_dm_only = True
defaultChannel = 0
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
cmdBang = False # require ! to be the first character in a command
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
```
### Location Settings
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
```ini
[location]
enabled = True
lat = 48.50
lon = -123.0
# To fuzz the location of the above
fuzzConfigLocation = True
# Fuzz all values in all data
fuzzItAll = False
UseMeteoWxAPI = True
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
coastalForecastDays = 3 # number of data points to return, default is 3
```
### Module Settings
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
```ini
[bbs]
enabled = False
[general]
DadJokes = False
StoreForward = False
```
### History
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
```ini
enableCmdHistory = True # history command enabler
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
```
### Sentry Settings
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
```ini
SentryEnabled = True # detect anyone close to the bot
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
highFlyingAlert = True # HighFlying Node alert
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
```
### E-Mail / SMS Settings
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
```ini
[smtp]
# enable or disable the SMTP module, minimum required for outbound notifications
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
sysopEmails =
# See config.template for all the SMTP settings
SMTP_SERVER = smtp.gmail.com
SMTP_AUTH = True
EMAIL_SUBJECT = Meshtastic✉
```
### Emergency Response Handler
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
```ini
[emergencyHandler]
enabled = True # enable or disable the emergency response handler
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
alert_interface = 1
```
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS and NINA
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
```ini
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
ignoreFEMAenable = True # Ignore any headline that includes followig word list
ignoreFEMAwords = test,exercise
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# To use other country services enable only a single optional serivce
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
myRegionalKeysDE = 110000000000,120510000000
```
#### NOAA EAS
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
```ini
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
ignoreEASenable = True # Ignore any headline that includes followig word list
ignoreEASwords = test,advisory
```
#### USGS River flow data and Volcano alerts
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
Volcano Alerts use lat/long to determine ~1000km radius
```ini
[location]
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
riverList = 14144700 # example Mouth of Columbia River
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
```
### Repeater Settings
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
```ini
[repeater] # repeater module
enabled = True
repeater_channels = [2, 3]
```
### Ollama (LLM/AI) Settings
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
```ini
# Enable ollama LLM see more at https://ollama.com
ollama = True # Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
```
Also see `llm.py` for changing the defaults of:
```ini
# LLM System Variables
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
# Used in the meshBotAI template (legacy)
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
### Wikipedia Search Settings
The Wikipedia search module can use either the online Wikipedia API or a local Kiwix server for offline wiki access. Kiwix is especially useful for mesh networks operating in remote or offline environments.
```ini
# Enable or disable the wikipedia search module
wikipedia = True
# Use local Kiwix server instead of online Wikipedia
# Set to False to use online Wikipedia (default)
useKiwixServer = False
# Kiwix server URL (only used if useKiwixServer is True)
kiwixURL = http://127.0.0.1:8080
# Kiwix library name (e.g., wikipedia_en_100_nopic_2024-06)
# Find available libraries at https://library.kiwix.org/
kiwixLibraryName = wikipedia_en_100_nopic_2024-06
```
To set up a local Kiwix server:
1. Install Kiwix tools: https://kiwix.org/en/ `sudo apt install kiwix-tools -y`
2. Download a Wikipedia ZIM file to `data/`: https://library.kiwix.org/ `wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_nopic_2025-09.zim`
3. Run the server: `kiwix-serve --port 8080 wikipedia_en_100_nopic_2025-09.zim`
4. Set `useKiwixServer = True` in your config.ini
The bot will automatically extract and truncate content to fit Meshtastic's message size limits (~500 characters).
### Radio Monitoring
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```ini
[radioMon]
enabled = True
rigControlServerAddress = localhost:4532
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
signalHoldTime = 10 # hold time for high SNR
signalCooldown = 5 # the following are combined to reset the monitor
signalCycleLimit = 5
```
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
filemon_enabled = True
file_path = alert.txt # text file to monitor for changes
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
enable_read_news = False # news command will return the contents of a text file
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
enable_runShellCmd = False # enable the use of exernal shell commands, this enables more data in `sysinfo` DM
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
allowXcmd = True
```
#### Offline EAS
To Monitor EAS with no internet connection see the following notes
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
no examples yet for these tools
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [dsame3](https://github.com/jamieden/dsame3)
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
```bash
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
```
The following example shell command will pipe rtl_sdr to alert.txt
```bash
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
#### Newspaper on mesh
Maintain multiple news sources. Each source should be a file named `{source}_news.txt` in the `data/` directory (for example, `data/mesh_news.txt`).
- To read the default news, use the `readnews` command (reads from `data/news.txt`.
- To read a specific source, use `readnews abc` to read from `data/abc_news.txt`.
This allows you to organize and access different news feeds or categories easily.
External scripts can update these files as needed, and the bot will serve the latest content on request.
### Greet new nodes QRZ module
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
```ini
[qrz]
enabled = True # QRZ Hello to new nodes
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
training = True # Training mode will not send the hello message to new nodes, use this to build up database
```
### Scheduler
In the config.ini enable the module
```ini
[scheduler]
enabled = False # enable or disable the scheduler module
interface = 1 # channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
interval = # interval to use when time is not set (e.g. every 2 days)
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
```
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"switchON": lambda: call_external_script(message)
#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 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))
```
This would call the default script located in script/runShell.sh and return its output.
#### 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 message just needs to have bbslink
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
```
```ini
bbslink_enabled = True
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
```
Happy meshing!

132
modules/adding_more.md Normal file
View File

@@ -0,0 +1,132 @@
# Modules and Adding Features
This document explains how to add new modules and commands to your Meshtastic mesh-bot project.
## Table of Contents
- [Overview](#overview)
- [Adding a New Command](#adding-a-new-command)
- [Running a Shell Command](#running-a-shell-command)
- [Best Practices](#best-practices)
- [Technical Assistance & Troubleshooting](#technical-assistance--troubleshooting)
---
## Overview
For code testing, see `etc/simulator.py` to simulate a bot.
You can also use `meshtasticd` (Linux-native) in `noradio` mode with MQTT server and client to emulate a mesh network.
---
## Adding a New Command
Follow these steps to add a new BBS option or command to the bot:
### 1. Define the Command Handler
Add a new function in `mesh_bot.py` to handle your command.
Example for a command called `newcommand`:
```python
def handle_newcommand(message, message_from_id, deviceID):
return "This is a response from the new command."
```
If your command is complex, consider creating a new module (e.g., `modules/newcommand.py`).
Import your new module where needed (see `modules/system.py` for examples).
---
### 2. Add the Command to the Auto Response
Update the `auto_response` function in `mesh_bot.py` to include your new command:
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
#...
```
---
### 3. Update the Trap List and Help
Edit `modules/system.py` to include your new command in the trap list and help message:
```python
#...
trap_list = ("cmd", "cmd?", "newcommand") # Add your command here
help_message = "Bot CMD?:newcommand, "
#...
```
**Preferred method:**
Add a configuration block below `ping` (around line 28):
```python
# newcommand Configuration
newcommand_enabled = True # settings.py handles config.ini values; this is a placeholder
if newcommand_enabled:
trap_list_newcommand = ("newcommand",)
trap_list = trap_list + trap_list_newcommand
help_message = help_message + ", newcommand"
```
---
### 4. Test the New Command
Run MeshBot and test your new command by sending a message with `newcommand` to ensure it responds correctly.
---
## Running a Shell Command
You can make a command that calls a bash script on the system (requires the `filemon` module):
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"switchON": lambda: call_external_script(message)
```
This will call the default script located at `script/runShell.sh` and return its output.
---
## Best Practices
- **Modularize:** Place complex or reusable logic in its own module.
- **Document:** Add docstrings and comments to your functions.
- **Test:** Use the simulator or a test mesh to verify new features.
- **Update Help:** Keep the help message and trap list up to date for users.
- **Configuration:** Use `settings.py` and `config.ini` for feature toggles and settings.
---
## Technical Assistance & Troubleshooting
- **Debug Logging:**
Use the `logger` module for debug output. Check logs for errors or unexpected behavior.
- **Common Issues:**
- *Module Import Errors:* Ensure your new module is in the `modules/` directory and imported correctly.
- *Command Not Responding:* Verify your command is in the trap list and auto_response dictionary.
- *Configuration Problems:* Double-check `settings.py` and `config.ini` for typos or missing entries.
- **Testing:**
- Use `etc/simulator.py` for local testing without radio hardware.
- Use `meshtasticd` in `noradio` mode for network emulation.
- **Python Environment:**
- Use a virtual environment (`python3 -m venv venv`) to manage dependencies.
- Install requirements with `pip install -r requirements.txt`.
- **Updating Dependencies:**
- try not to I want to remove some.
- **Getting Help:**
- Check the project wiki or issues page for common questions.
- Use inline comments and docstrings for clarity.
- If youre stuck, ask for help on the projects GitHub Discussions or Issues tab.
---
Happy hacking!

189
modules/bbstools.md Normal file
View File

@@ -0,0 +1,189 @@
---
# 📡 meshBBS: How-To & API Documentation
This document covers the Bulliten Board System or BBS componment of the meshing-around project.
## Table of Contents
1. [BBS Core Functions](#1-bbs-core-functions)
- [Direct Messages (DMs)](#11-direct-messages-dms)
2. [BBS Database Sync: File-Based (Out-of-Band)](#1-bbs-database-sync-file-based-out-of-band)
3. [BBS Over-the-Air (OTA) Sync: Linking](#2-bbs-over-the-air-ota-sync-linking)
4. [Scheduling BBS Sync](#3-scheduling-bbs-sync)
5. [Best Practices](#4-best-practices)
6. [Example: Full Sync Workflow](#5-example-full-sync-workflow)
7. [Troubleshooting](#6-troubleshooting)
8. [API Reference: BBS Sync](#7-api-reference-bbs-sync)
## 1. **BBS Core Functions**
## 1.1 **Direct Messages (DMs)**
### **How DMs Work**
- Direct Messages (DMs) are private messages sent from one node to another.
- DMs are stored separately from public posts in `data/bbsdm.pkl`.
- Each DM entry in the pickle, typically includes: `[id, toNode, message, fromNode, timestamp, threadID, replytoID]`.
### **DM Delivery**
- When a DM is posted using `bbs_post_dm(toNode, message, fromNode)`, it is added to the recipient's DM database.
- DMs can be delivered in two ways:
1. **File-Based Sync:**
- The `bbsdm.pkl` file is copied between nodes using SCP, rsync, or other file transfer methods.
- After syncing, the recipient node can check for new DMs using `bbs_check_dm(toNode)`.
2. **Over-the-Air (OTA) Sync:**
- DMs can be exchanged between nodes using the same OTA sync mechanism as other posts.
- The bot will receive (onRX) or detect any packet and deliver the DM/mail to the recipient.
- DMs are only visible to the intended recipient node and are not listed in the public message list.
### **DM Commands**
| Command | Description |
|-----------------|---------------------------------------------|
| `bbs_post_dm` | Send a direct message to another node |
| `bbs_check_dm` | Check for new DMs for your node |
| `bbs_delete_dm` | Delete a DM after reading |
---
### **Message Storage**
The .. database is
- Messages are stored in `data/bbsdb.pkl` (public posts) and `data/bbsdm.pkl` (direct messages).
- Format: Each message is a list, e.g. `[id, subject, body, fromNode, timestamp, threadID, replytoID]`.
| Command | Description |
|--------------|-----------------------------------------------|
| `bbshelp` | Show BBS help |
| `bbslist` | List messages |
| `bbsread` | Read a message by ID |
| `bbspost` | Post a message or DM |
| `bbsdelete` | Delete a message |
| `bbsinfo` | BBS stats (sysop) |
| `bbslink` | Link messages between BBS systems |
---
Enable in `[bbs]` section of `config.ini`.
## 1. **BBS Database Sync: File-Based (Out-of-Band)**
### **Manual/Automated File Sync (e.g., SSH/SCP)**
- **Purpose:** Sync BBS data between nodes by copying `bbsdb.pkl` and `bbsdm.pkl` files.
- **How-To:**
1. **Locate Files:**
- `data/bbsdb.pkl` (public posts)
- `data/bbsdm.pkl` (direct messages)
2. **Copy Files:**
Use `scp` or `rsync` to copy files between nodes:
```sh
scp user@remote:/path/to/meshing-around/data/bbsdb.pkl ./data/bbsdb.pkl
scp user@remote:/path/to/meshing-around/data/bbsdm.pkl ./data/bbsdm.pkl
```
3. **Reload Database:**
After copying, when the "API" is enabled the watchdog will look for changes and injest.
- **Automating with Cron/Scheduler:**
- Set up a cron job or use the bots scheduler to periodically pull/push files.
---
## 2. **BBS Over-the-Air (OTA) Sync: Linking**
### **How OTA Sync Works**
- Nodes can exchange BBS messages using special commands over the mesh network.
- Uses `bbslink` and `bbsack` commands for message exchange.
- Future supports compression for bandwidth efficiency.
### **Enabling BBS Linking**
- Set `bbs_link_enabled = True` in your config.
- Optionally, set `bbs_link_whitelist` to restrict which nodes can sync.
### **Manual Sync Command**
- To troubleshoot request sync from another node, send:
```
bbslink <messageID> $<subject> #<body>
```
- The receiving node will respond with `bbsack <messageID>`.
### **Out-of-Band Channel**
- For high-reliability sync, configure a dedicated channel (not used for chat).
---
## 3. **Scheduling BBS Sync**
### **Using the Bots Scheduler**
- You can schedule periodic sync requests to a peer node.
- Example: Every hour, send a `bbslink` request to a peer.
see more at [Module Readme](README.md#scheduler)
---
## 4. **Best Practices**
- **Backup:** Regularly back up `bbsdb.pkl` and `bbsdm.pkl`.
- **Security:** Use SSH keys for file transfer; restrict OTA sync to trusted nodes.
- **Reliability:** Use a dedicated channel for BBS sync to avoid chat congestion.
- **Automation:** Use the scheduler for regular syncs, both file-based and OTA.
---
## 5. **Example: Full Sync Workflow**
1. **Set up a dedicated sync channel** (e.g., channel bot-admin).
2. **Configure both nodes** with `bbs_link_enabled = True` and add each other to `bbs_link_whitelist`.
3. **Schedule sync** every hour:
- Node A sends `bbslink 0` to Node B on channel 99.
- Node B responds with messages and `bbsack`.
4. **Optionally, use SSH/scp** to copy `bbsdb.pkl` for full out-of-band backup.
---
## 6. **Troubleshooting**
- **Messages not syncing?**
- Check `bbs_link_enabled` and whitelist settings.
- Ensure both nodes are on the same sync channel.
- Check logs for errors.
- **File sync issues?**
- Verify file permissions and paths.
- Ensure the bot reloads the database after file copy.
## 7. **API Reference: BBS Sync**
### **Key Functions in Python**
| Function | Purpose | Usage Example |
|-------------------------|-------------------------------------------|----------------------------------------------------|
| `bbs_post_message()` | Post a new public message | `bbs_post_message(subject, body, fromNode)` |
| `bbs_read_message()` | Read a message by ID | `bbs_read_message(messageID)` |
| `bbs_delete_message()` | Delete a message (admin/owner only) | `bbs_delete_message(messageID, fromNode)` |
| `bbs_list_messages()` | List all message subjects | `bbs_list_messages()` |
| `bbs_post_dm()` | Post a direct message | `bbs_post_dm(toNode, message, fromNode)` |
| `bbs_check_dm()` | Check for DMs for a node | `bbs_check_dm(toNode)` |
| `bbs_delete_dm()` | Delete a DM after reading | `bbs_delete_dm(toNode, message)` |
| `get_bbs_stats()` | Get stats on BBS and DMs | `get_bbs_stats()` |
| Function | Purpose |
|---------------------------|-------------------------------------------|
| `bbs_sync_posts()` | Handles incoming/outgoing sync requests |
| `bbs_receive_compressed()`| Handles compressed sync data |
| `compress_data()` | Compresses data for OTA transfer |
| `decompress_data()` | Decompresses received data |
### **Handle Incoming Sync**
- The bot automatically processes incoming `bbslink` and `bbsack` commands via `bbs_sync_posts()`.
### **Compressed Sync**
Future Use
- If `useSynchCompression` is enabled, use:
```python
compressed = compress_data(msg)
send_raw_bytes(peerNode, compressed)
```
- Receiving node uses `bbs_receive_compressed()`.
---
---

View File

@@ -4,6 +4,13 @@
import pickle # pip install pickle
from modules.log import *
import time
from datetime import datetime
useSynchCompression = False
if useSynchCompression:
import zlib
from modules.system import send_raw_bytes
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
@@ -11,6 +18,7 @@ trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsin
bbs_messages = []
bbs_dm = []
def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
@@ -88,7 +96,11 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
else:
return "Please specify a message number to delete."
def bbs_post_message(subject, message, fromNode):
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
# post a message to the bbsdb
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
thread = threadID
replyto = replytoID
# post a message to the bbsdb and assign a messageID
messageID = len(bbs_messages) + 1
@@ -106,7 +118,7 @@ def bbs_post_message(subject, message, fromNode):
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
@@ -197,6 +209,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
@@ -241,7 +279,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

@@ -6,6 +6,7 @@ import asyncio
import random
import os
import subprocess
from datetime import datetime, timedelta
trap_list_filemon = ("readnews",)
@@ -69,22 +70,33 @@ async def watch_file():
return content
await asyncio.sleep(1) # Check every
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
def call_external_script(message, script="runShell.sh"):
# If no path is given, assume script/ directory
if "/" not in script and "\\" not in script:
script = os.path.join("script", script)
try:
# Debugging: Print the current working directory and resolved script path
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
if not os.path.exists(script_path):
# try the raw script name
# Try the raw script name
script_path = script
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
return output
result = subprocess.run(
["bash", script_path, message],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
logger.error(f"FileMon: Script error: {result.stderr.strip()}")
return None
output = result.stdout.strip()
return output if output else None
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
return None

541
modules/games/README.md Normal file
View File

@@ -0,0 +1,541 @@
# Meshtastic Mesh-Bot Games
## Game Index
- [Blackjack](#blackjack-game-module)
- [DopeWars](#dopewars-game-module)
- [GolfSim](#golfsim-game-module)
- [Lemonade Stand](#lemonade-stand-game-module)
- [Tic-Tac-Toe](#tic-tac-toe-game-module)
- [MasterMind](#mastermind-game-module)
- [Video Poker](#video-poker-game-module)
- [Word of the Day Game](#word-of-the-day-game--rules--features)
---
# Blackjack Game Module
This module implements a classic game of Blackjack (Casino 21) for the Meshtastic mesh-bot.
## How to Play
- **Start the Game:**
Send the command `blackjack` via DM to the bot to start a new game session.
- **Place a Bet:**
When prompted, enter the amount you wish to wager (e.g., `5`). Minimum bet is 1 chip, maximum is your current chip total.
- **Gameplay Commands:**
After betting, you will be dealt two cards. The dealer will also have two cards (one face up).
- `h` or `hit` — Draw another card.
- `s` or `stand` — End your turn and let the dealer play.
- `d` or `double` — Double your bet and draw one more card (if you have enough chips).
- `f` or `forfit` — Forfeit half your bet and end the round.
- `r` or `resend` — Resend your current hand status.
- `l` or `leave` — Leave the table and end your session.
- **Winning:**
- Get as close to 21 as possible without going over.
- If your hand exceeds 21, you bust and lose your bet.
- If you beat the dealer without busting, you win your bet.
- If you get a Blackjack (21 with two cards), you win 1.5x your bet.
- If you tie the dealer, it's a push (no win/loss).
- **High Scores:**
The module tracks the highest chip total achieved. If you beat the high score, you'll be notified!
## Notes
- Each player starts with 100 chips.
- If you run out of chips, your balance will reset to 100.
- The game state is tracked per player using your node ID.
- Game progress and high scores are saved in `data/blackjack_hs.pkl`.
- Only one game session per player is supported at a time.
- For best results, play via DM to avoid interfering with other users' sessions.
## Example Session
```
You have 100 chips. Whats your bet?
> 10
Player[14] 8♠, 6♥
Dealer[10] 10♦
🧠Hit: 38% 👎, 62% 👍
(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table
> h
Player[18] 8♠, 6♥, 4♣
Dealer[10] 10♦
🧠Hit: 77% 👎, 23% 👍
[H,S,F,D]
> s
Player[18] 8♠, 6♥, 4♣
Dealer[20] 10♦, Q♠
👎DEALER WINS
📊🏆P:0,D:1,T:0
💰You have 90 chips
Bet or Leave?
```
## Credits
- Ported from [Himan10/BlackJack](https://github.com/Himan10/BlackJack)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# DopeWars Game Module
A text-based trading game inspired by the classic DopeWars/DrugWars, adapted for the Meshtastic mesh-bot.
## How to Play
- **Start the Game:**
Send the command `dopewars` via DM to the bot to begin a new session.
- **Objective:**
Travel between cities, buy and sell drugs, and try to maximize your cash in 7 days.
- **Game Flow:**
1. **Pick a Starting City:**
Youll be shown a list of cities. Enter the number to choose your starting location.
2. **Each Day:**
- Youll see drug prices, your inventory, and your cash.
- You can buy, sell, or fly to a new city.
- Random events may occur (police, market changes, or finding cash/drugs).
3. **Commands:**
- **Buy:** `b,drug#,qty#` (e.g., `b,1,10` buys 10 of drug 1)
- **Sell:** `s,drug#,qty#` (e.g., `s,2,5` sells 5 of drug 2)
- **Max:** Use `m` for max quantity (e.g., `b,1,m`)
- **Sell All:** Just `s` to sell everything you have.
- **Fly:** `f` to move to a new city (ends the day).
- **Price List:** `p` to view current prices and inventory.
- **End Game:** `e` to end your run early.
4. **Repeat:**
Each time you fly, a day passes. After 7 days, your final cash is your score.
- **Winning:**
- Try to finish with as much cash as possible.
- Beat the high score to be crowned the top dealer!
## Example Session
```
1. Red Deer 2. Edmonton 3. Calgary 4. Toronto 5. Vancouver 6. St. Johns Where do you want to 🛫?#
> 2
🗺Edmonton 📆1/7 🎒0/100 💵5,000
#1.Cocaine$15,000(0) #2.Heroin$2,500(0) #3.Weed$800(0) ...
Buy💸, Sell💰, (F)ly🛫? (P)riceList?
> b,2,10
Heroin: you have🎒 0 The going price is: $2,500
You bought 10 Heroin. Remaining cash: $47,500
Buy💸, Sell💰, Fly🛫?
> f
🗺Toronto 📆2/7 🎒10/100 💵47,500
...
```
## Notes
- You start with $5,000 and a 100-slot backpack.
- Each drug has a random price per city and day.
- Special events can spike or crash prices, or cause you to lose/gain cash or inventory.
- Police may confiscate your drugs or cash.
- High scores are saved in `data/dopewar_hs.pkl`.
- Only one game session per player at a time.
- Play via DM for best experience.
## Credits
- Ported from [Reconfirefly/drugwars](https://github.com/Reconfirefly/drugwars)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# GolfSim Game Module
A text-based golf simulator for the Meshtastic mesh-bot. Play a full 9-hole round, choose your clubs, and try to set a new course record!
## How to Play
- **Start the Game:**
Send the command `golf` via DM to the bot to begin a new round.
- **Objective:**
Complete 9 holes in as few strokes as possible.
- **Game Flow:**
1. **Each Hole:**
- The bot tells you the hole number, length, par, and any hazards or weather.
- Choose your club for each shot by typing its name or initial:
- `d` or `driver` — Longest club
- `l` or `low` — Low iron
- `m` or `mid` — Mid iron
- `h` or `high` — High iron
- `g` or `gap` — Gap wedge
- `w` or `wedge` — Lob wedge
- `c` or `caddy` — Get a caddy guess for club distances
- The bot will tell you how far you hit and how far remains.
- When youre within 20 yards, youll automatically putt to finish the hole.
2. **Scoring:**
- The bot tracks your strokes and score relative to par.
- After each hole, youll see your score for the hole and your running total.
3. **Hazards & Surprises:**
- Hazards (sand, water, trees, etc.) and random events may affect your shots.
- Critters or weather can cause unexpected results!
4. **End of Round:**
- After 9 holes, your total strokes and score to par are shown.
- If you set a new low score, youll be notified as the new club record holder!
## Example Session
```
⛳️#1 is a 410-yard Par 4.☀️
Choose your club.
> d
🏌Hit D 260yd.
You have 150yd. ⛳️
Club?[D, L, M, H, G, W]🏌️
> m
🏌Hit M Iron 170yd. Overshot the green!🚀
You have 20yd. ⛳️
Club?[D, L, M, H, G, W]🏌️
> w
🏌Hit L Wedge 30yd. You're on the green! After 2 putt(s), you're in for 5 strokes. +Bogey
You've hit a total of 5 strokes today, for +Bogey
...
🎉Finished 9-hole round⛳ 🏆New Club Record🏆
```
## Notes
- Play via DM for best experience.
- Hazards and weather are randomized for each hole.
- High scores are saved in `data/golfsim_hs.pkl`.
- Only one game session per player at a time.
- Commands are not case-sensitive; you can use full club names or initials.
## Credits
- Ported from [danfriedman30/pythongame](https://github.com/danfriedman30/pythongame)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# Lemonade Stand Game Module
A text-based business simulation where you run your own lemonade stand! Buy supplies, set prices, and try to maximize your profits over a summer season.
## How to Play
- **Start the Game:**
Send the command `lemonade` via DM to the bot to begin a new game.
- **Objective:**
Make as much money as possible in 7 weeks by managing your lemonade stand.
- **Game Flow:**
1. **Each Week:**
- The bot will show you the weather, temperature, and sales potential.
- Buy supplies: cups, lemons, and sugar. Enter the number of each to purchase, or `n` for none.
- Set your selling price per cup.
- The bot will simulate sales and show your results, profits, and remaining inventory.
- Repeat for each week.
2. **Commands:**
- Enter a number to buy supplies or set price.
- Use `n` to skip buying an item.
- Enter `g` during pricing to go back and buy more supplies.
- At the end of each week, choose to continue or end the game.
3. **Scoring:**
- Your score is based on your net profit and efficiency (profit vs. possible profit).
- High scores are tracked and displayed at the end of the game.
## Example Session
```
LemonStand🍋Week #1 of 7. 85ºF Sunny ☀️
SupplyCost $0.45 a cup.
Sales Potential: 60 cups.
Inventory: 🥤:0 🍋:0 🍚:0
Prices:
🥤:$2.50 📦 of 25.
🍋:$4.00 🧺 of 8.
🍚:$3.00 bag for 15🥤.
💵:$30.00
🥤 to buy?
Have 0 Cost $2.50 a 📦 of 25
> 2
Purchased 2 📦 50 🥤 in inventory. $25.00 remaining
🍋 to buy?
Have 0🥤 of 🍋 Cost $4.00 a 🧺 for 8🥤
> 1
Purchased 1 🧺 8 🍋 in inventory. $21.00 remaining
🍚 to buy?
You have 0🥤 of 🍚, Cost $3.00 a bag for 15🥤
> 1
Purchased 1 bag(s) of 🍚 for $3.00. 15🥤🍚 in inventory.
Cost of goods is $0.45 per 🥤 $18.00 💵 remaining.
Price to Sell? or (G)rocery to buy more 🥤🍋🍚
> 1.25
Results Week📊#1 of 7 Cost/Price:$0.45/$1.25 P.Margin:$0.80 T.Sales:16@$1.25 G.Profit: $20.00 N.Profit:$12.80
Remaining 🥤:34 🍋:0 🍚:0 💵:$38.00📊P&L📈$8.00
Weekly📊#1. 16 sold x $1.25ea.
Play another week🥤? or (E)nd Game
```
## Notes
- You start with $30.00 and must buy supplies each week.
- Weather and temperature affect sales potential.
- If you run out of any supply, you can't sell more lemonade that week.
- High scores are saved in `data/lemonstand.pkl`.
- Only one game session per player at a time.
- Play via DM for best experience.
## Credits
- Ported from [tigerpointe/Lemonade-Stand](https://github.com/tigerpointe/Lemonade-Stand)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# Tic-Tac-Toe Game Module
A classic Tic-Tac-Toe game for the Meshtastic mesh-bot. Play against the bot, track your stats, and see if you can beat the AI!
## How to Play
- **Start the Game:**
Send the command `tictactoe` via DM to the bot to begin a new game.
- **Objective:**
Get three of your marks in a row (horizontally, vertically, or diagonally) before the bot does.
- **Game Flow:**
1. **Board Layout:**
- The board is numbered 1-9, left to right, top to bottom.
- Example:
```
1 | 2 | 3
4 | 5 | 6
7 | 8 | 9
```
2. **Making Moves:**
- On your turn, type the number (1-9) where you want to place your mark.
- The bot will respond with the updated board and make its move.
3. **Commands:**
- `n` — Start a new game.
- `e` or `q` — End the current game.
- `b` — Show the current board.
- Enter a number (1-9) to make a move.
4. **Winning:**
- The first to get three in a row wins.
- If the board fills with no winner, its a tie.
## Example Session
```
❌ | 2 | 3
4 | ⭕️ | 6
7 | 8 | 9
Your turn! Pick 1-9:
> 3
❌ | 2 | ❌
4 | ⭕️ | 6
7 | 8 | 9
🤖Bot wins! (n)ew (e)nd
```
## Notes
- Emojis are used for X and O unless disabled in settings.
- Your win/loss stats are tracked across games.
- The bot will try to win, block you, or pick a random move.
- Play via DM for best experience.
- Only one game session per player at a time.
## Credits
- Written for Meshtastic mesh-bot by Martin
# MasterMind Game Module
A text-based version of the classic code-breaking game MasterMind for the Meshtastic mesh-bot. Try to guess the secret color code in as few turns as possible!
## How to Play
- **Start the Game:**
Send the command `mmind` via DM to the bot to begin a new game.
- **Objective:**
Guess the secret 4-color code in 10 turns or less.
- **Game Flow:**
1. **Choose Difficulty:**
- (N)ormal: 4 colors (R🔴, Y🟡, G🟢, B🔵)
- (H)ard: 6 colors (R🔴, Y🟡, G🟢, B🔵, O🟠, P🟣)
- e(X)pert: 8 colors (R🔴, Y🟡, G🟢, B🔵, O🟠, P🟣, W⚪, K⚫)
- Type `n`, `h`, or `x` to select.
2. **Guessing:**
- Enter a 4-letter code using the color initials (e.g., `RGBY`).
- The bot will respond with feedback:
- ✅ color ✅ position: correct color in the correct spot
- ✅ color 🚫 position: correct color, wrong spot
- 🚫No pins: none of your colors are in the code
- You have 10 turns to guess the code.
3. **Winning:**
- Guess the code exactly to win!
- Your number of turns is tracked for high scores.
- After a win or loss, you can play again by choosing a difficulty.
## Example Session
```
The colors to choose from are:
R🔴, Y🟡, G🟢, B🔵
Enter your guess (e.g., RGBY):
> RGYB
Turn 1:
Guess🔴🟢🟡🔵
✅ color ✅ position: 2
✅ color 🚫 position: 1
> RYGB
Turn 2:
🏆Correct🔴🟡🟢🔵
You are the master mind!🤯
🏆 High Score:2 turns, Difficulty:n
Would you like to play again? (N)ormal, (H)ard, or e(X)pert?
```
## Notes
- Only one game session per player at a time.
- High scores are saved in `data/mmind_hs.pkl`.
- Play via DM for best experience.
- Input is not case-sensitive, but guesses must be exactly 4 letters.
## Credits
- Ported from [pwdkramer/pythonMastermind](https://github.com/pwdkramer/pythonMastermind)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# Video Poker Game Module
A text-based Video Poker game for the Meshtastic mesh-bot. Play classic five-card draw poker, place your bets, and try to build your bankroll!
## How to Play
- **Start the Game:**
Send the command `videopoker` via DM to the bot to begin a new session.
- **Objective:**
Win as many coins as possible by making the best poker hands.
- **Game Flow:**
1. **Place Your Bet:**
- You start with 20 coins.
- Enter your bet (1-5 coins) to begin each hand.
2. **Draw Cards:**
- You are dealt 5 cards.
- The bot will show your hand and a hint about its strength.
3. **Redraw:**
- Choose which cards to replace:
- Enter numbers (e.g., `1,3,4`) to redraw those cards.
- Enter `a` to redraw all cards.
- Enter `n` to keep your current hand.
- Enter `h` to show your hand again.
- You can only redraw once per hand.
4. **Scoring:**
- After the redraw, your hand is scored and winnings are paid out based on the hand type.
- If you run out of coins, your balance resets to 20.
- High scores are tracked and announced.
5. **Continue:**
- Place another bet to play again, or enter `l` to leave the table.
## Example Session
```
You have 20 coins,
Whats your bet?
> 5
K♠ 7♦ 7♣ 2♥ 9♠
Showing:Pair👯
Deal new card?
ex: 1,3,4 or (N)o,(A)ll (H)and
> 1,4
7♦ 7♣ 9♠ 3♣ Q♥
Your hand, Pair👯. Your bankroll is now 22 coins.
Place your Bet, or (L)eave Table.
```
## Hand Rankings & Payouts
- 👑Royal Flush🚽 — 10x bet
- 🧻Straight Flush🚽 — 9x bet
- Flush🚽 — 8x bet
- Full House🏠 — 7x bet
- Four of a Kind👯👯 — 6x bet
- Three of a Kind☘ — 5x bet
- Two Pair👯👯 — 4x bet
- Straight📏 — 3x bet
- Pair👯 — 2x bet
- Bad Hand 🙈 — Lose bet
## Notes
- Only one game session per player at a time.
- High scores are saved in `data/videopoker_hs.pkl`.
- Play via DM for best experience.
- Bets must be between 1 and 5 coins and not exceed your bankroll.
## Credits
- Ported from [devtronvarma/Video-Poker-Terminal-Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
# Word of the Day Game — Rules & Features
- **Word of the Day:**
Each day, a new word is chosen from `data/wotd.json` (or a default list if missing). Mention the word (or its leet/1337 variants) in chat to win and trigger a new word.
- **Bingo Mini-Game:**
A random 3x3 bingo card of words, drawn from `data/bingo.json` (or a default list if missing). Mention words from the card in chat. Complete a row, column, or diagonal to win BINGO and get a new card.
- **Emoji Mini-Game:**
Use emojis in chat to:
- Play a slot machine: send the same emoji several times in a row to hit the JACKPOT!
- **Data Files:**
- `data/wotd.json`: List of words and definitions for the Word of the Day.
[
{
"word": "serendipity",
"meta": "The occurrence of events by chance in a happy or beneficial way."
},
{
"word": "ephemeral",
"meta": "Lasting for a very short time."
},
{
"word": "sonder",
"meta": "The realization that each passerby has a life as vivid and complex as your own."
}
]
- `data/bingo.json`: List of words for bingo cards.
[
"dog",
"cat",
"fish",
"bird",
"hamster",
"rabbit",
"turtle",
"lizard",
"snake"
]

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', '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':[],'last_played': time.time()}]
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', '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"Welcome to ♠BlackJack♣ you have {p_chips.total} chips. Whats your bet?"
if nodeID != 0:
#logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"You have {p_chips.total} chips. Whats your bet?"
return "Error: Player not found."
if getLastCmdJack(nodeID) == "new":
# Place Bet
@@ -283,24 +297,26 @@ def playBlackJack(nodeID, message):
#resend the hand
msg += show_some(p_cards, d_cards, p_hand)
return msg
elif "blackjack" in message.lower():
return f"\nTo place a bet, enter the amount you wish to wager."
else:
try:
bet_money = int(message)
except ValueError:
return "Invalid Bet, please enter a valid number."
return f"\nInvalid Bet, please enter a valid number."
if bet_money <= p_chips.total and bet_money >= 1:
p_chips.bet = bet_money
else:
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
return f"\nInvalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
except ValueError:
return f"Invalid Bet, the maximum bet, {p_chips.total}"
return f"\nInvalid Bet, the maximum bet, {p_chips.total}"
# Show the cards
msg += show_some(p_cards, d_cards, p_hand)
# check for blackjack 21 and only two cards
if p_hand.value == 21 and len(p_hand.cards) == 2:
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
msg += f"\n🎰 BLAAAACKJACKKKK 💰"
p_chips.total += round(p_chips.bet * 1.5)
setLastCmdJack(nodeID, "dealerTurn")
blackJack = True
@@ -317,7 +333,7 @@ def playBlackJack(nodeID, message):
if getLastCmdJack(nodeID) == "betPlaced":
setLastCmdJack(nodeID, "playing")
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
msg += f"\n(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
# save the game state
for i in range(len(jackTracker)):
@@ -367,7 +383,7 @@ def playBlackJack(nodeID, message):
# Check if player bust
if player_bust(p_hand, p_chips):
d_win += 1
msg += "💥PlayerBUST💥"
msg += f"\n💥PlayerBUST💥"
setLastCmdJack(nodeID, "dealerTurn")
if getLastCmdJack(nodeID) == "playing":
@@ -419,7 +435,7 @@ def playBlackJack(nodeID, message):
d_hand.add_cards(d_card)
if dealer_bust(d_hand, p_hand, p_chips):
p_win += 1
msg += "💰DealerBUST💥"
msg += f"\n💰DealerBUST💥"
break
# Show all cards
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
@@ -427,15 +443,15 @@ def playBlackJack(nodeID, message):
# Check who wins
if push(p_hand, d_hand):
draw += 1
msg += "👌PUSH"
msg += f"\n👌PUSH"
elif player_wins(p_hand, d_hand, p_chips):
p_win += 1
msg += "🎉PLAYER WINS🎰"
msg += f"\n🎉PLAYER WINS🎰"
elif dealer_wins(p_hand, d_hand, p_chips):
d_win += 1
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
else:
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
# Display the Game Stats
msg += gameStats(str(p_win), str(d_win), str(draw))
@@ -443,20 +459,20 @@ def playBlackJack(nodeID, message):
# Display the chips left
if p_chips.total < 1:
if p_chips.total > 0:
msg += "🪙Keep the change you filthy animal!"
msg += f"\n🪙Keep the change you filthy animal!"
else:
msg += "💸NO MORE CHIPS!🏧💳"
msg += f"\n💸NO MORE CHIPS!🏧💳"
p_chips.total = jack_starting_cash
else:
# check high score
highScore = loadHSJack()
if highScore != 0 and p_chips.total > highScore['highScore']:
msg += f"💰HighScore💰{p_chips.total} "
msg += f"\n💰HighScore💰{p_chips.total} "
saveHSJack(nodeID, p_chips.total)
else:
msg += f"💰You have {p_chips.total} chips "
msg += f"\n💰You have {p_chips.total} chips "
msg += " Bet or Leave?"
msg += f"\nBet or Leave?"
# Reset the game
setLastCmdJack(nodeID, "new")

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

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
@@ -146,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
@@ -194,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
@@ -321,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
@@ -366,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
@@ -404,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

@@ -46,15 +46,27 @@ lameJokes = [
"Chuck Norris can kill two stones with one bird.",
"Chuck Norris can speak braille.",
"Chuck Norris can build a snowman out of rain.",
"Chuck Norris can hear sign language.",
"Death once had a near-Chuck Norris experience.",
"Chuck Norris can unscramble an egg.",
"Chuck Norris can win a game of Connect Four in only three moves.",
"Chuck Norris can make a snowman out of rain.",
"Chuck Norris can strangle you with a cordless phone.",
"Chuck Norris can do a wheelie on a unicycle.",
"Chuck Norris can kill two stones with one bird."]
"This is a test. A test of the Joke Brodcast System. If this had been an actual joke, you would have been amused.",
"Chuck Norris doesn't join mesh networks. Mesh networks join Chuck's topology.",
"Every time Chuck Norris sends a packet, it arrives before he hits 'send'",
"Chuck Norris doesn't need LoRa. His roundhouse kick has a 15km range with zero latency.",
"When Chuck Norris uses a node, the bandwidth doubles out of fear.",
"Chuck Norris once pinged a device. It replied with an apology and a firmware update.",
"Chuck Norris doesn't use AES encryption. His packets are so secure, they punch hackers in the bits.",
"The Meshtastic protocol has a hidden mode: “Chuck Norris mode.” It only activates when he blinks.",
"Chuck Norris doesn't need a GPS fix. Satellites triangulate themselves around him.",
"Chuck Norris's mesh node doesn't sleep. It meditates while transmitting at full power.",
"Chuck Norris doesn't broadcast. He declares.",
"Chuck Norris once bridged two mesh networks using a shoelace.",
"Chuck Norris's packets don't hop. They teleport out of respect.",
"Chuck Norris doesn't need a repeater. Client_Mute is set to 'Always'.",
"Chuck Norris's mesh messages are entangled. When he sends one, it's already received.",
"Chuck Norris doesn't mesh with others. Others mesh with Chuck.",
"Chuck Norris's node doesn't need a case. The PCB is armored with his beard hair.",
"Chuck Norris once typed “Hello World” and the world replied 'Hello Chuck.'",
]
# pylint: disable=C0103, W0612
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
def tableOfContents():
@@ -88,8 +100,8 @@ def tableOfContents():
'plant': '🌱', 'tree': '🌳', 'flower': '🌸', 'leaf': '🍃', 'cactus': '🌵', 'mushroom': '🍄', 'herb': '🌿', 'bamboo': '🎍', 'rose': '🌹', 'tulip': '🌷', 'sunflower': '🌻',
'hibiscus': '🌺', 'cherry blossom': '🌸', 'bouquet': '💐', 'seedling': '🌱', 'palm tree': '🌴', 'evergreen tree': '🌲', 'deciduous tree': '🌳', 'fallen leaf': '🍂', 'maple leaf': '🍁',
'ear of rice': '🌾', 'shamrock': '☘️', 'four leaf clover': '🍀', 'grapes': '🍇', 'melon': '🍈', 'watermelon': '🍉', 'tangerine': '🍊', 'lemon': '🍋', 'banana': '🍌', 'pineapple': '🍍',
'mango': '🥭', 'apple': '🍎', 'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
'eggplant': '🍆', 'potato': '🥔', 'carrot': '🥕', 'corn': '🌽', 'hot pepper': '🌶️', 'cucumber': '🥒', 'leafy green': '🥬', 'broccoli': '🥦', 'garlic': '🧄', 'onion': '🧅',
'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
'hot pepper': '🌶️', 'cucumber': '🥒', 'leafy green': '🥬', 'broccoli': '🥦', 'garlic': '🧄', 'onion': '🧅',
'peanuts': '🥜', 'chestnut': '🌰', 'bread': '🍞', 'croissant': '🥐', 'baguette': '🥖', 'flatbread': '🥙', 'pretzel': '🥨', 'bagel': '🥯', 'pancakes': '🥞', 'waffle': '🧇', 'cheese': '🧀',
'meat': '🍖', 'poultry': '🍗', 'bacon': '🥓', 'hamburger': '🍔', 'fries': '🍟', 'pizza': '🍕', 'hot dog': '🌭', 'sandwich': '🥪', 'taco': '🌮', 'burrito': '🌯', 'tamale': '🫔',
'stuffed flatbread': '🥙', 'falafel': '🧆', 'egg': '🥚', 'fried egg': '🍳', 'shallow pan of food': '🥘', 'pot of food': '🍲', 'fondue': '🫕', 'bowl with spoon': '🥣', 'green salad': '🥗',
@@ -103,19 +115,19 @@ def tableOfContents():
'globe with meridians': '🌐', 'world map': '🗺️', 'mountain': '⛰️', 'volcano': '🌋', 'mount fuji': '🗻', 'camping': '🏕️', 'beach with umbrella': '🏖️', 'desert': '🏜️', 'desert island': '🏝️',
'national park': '🏞️', 'stadium': '🏟️', 'classical building': '🏛️', 'building construction': '🏗️', 'brick': '🧱', 'rock': '🪨', 'wood': '🪵', 'hut': '🛖', 'houses': '🏘️', 'derelict house': '🏚️',
'house with garden': '🏡', 'office building': '🏢', 'japanese post office': '🏣', 'post office': '🏤', 'hospital': '🏥', 'bank': '🏦', 'hotel': '🏨', 'love hotel': '🏩', 'convenience store': '🏪',
'school': '🏫', 'department store': '🏬', 'factory': '🏭', 'japanese castle': '🏯', 'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '', 'mosque': '🕌',
'hindu temple': '🛕', 'synagogue': '🕍', 'shinto shrine': '⛩️', 'kaaba': '🕋', 'fountain': '', 'tent': '', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'barber pole': '💈',
'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '', 'mosque': '🕌',
'fountain': '', 'tent': '', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'barber pole': '💈',
'robot': '🤖', 'alien': '👽', 'ghost': '👻', 'skull': '💀', 'pumpkin': '🎃', 'clown': '🤡', 'wizard': '🧙', 'elf': '🧝', 'fairy': '🧚', 'mermaid': '🧜', 'vampire': '🧛',
'zombie': '🧟', 'genie': '🧞', 'superhero': '🦸', 'supervillain': '🦹', 'mage': '🧙', 'knight': '🛡️', 'ninja': '🥷', 'pirate': '🏴‍☠️', 'angel': '👼', 'devil': '😈', 'dragon': '🐉',
'unicorn': '🦄', 'phoenix': '🦅', 'griffin': '🦅', 'centaur': '🐎', 'minotaur': '🐂', 'cyclops': '👁️', 'medusa': '🐍', 'sphinx': '🦁', 'kraken': '🦑', 'yeti': '❄️', 'sasquatch': '🦧',
'loch ness monster': '🦕', 'chupacabra': '🐐', 'banshee': '👻', 'golem': '🗿', 'djinn': '🧞', 'basilisk': '🐍', 'hydra': '🐉', 'cerberus': '🐶', 'chimera': '🐐', 'manticore': '🦁', 'wyvern': '🐉',
'pegasus': '🦄', 'hippogriff': '🦅', 'kelpie': '🐎', 'selkie': '🦭', 'kitsune': '🦊', 'tanuki': '🦝', 'tengu': '🦅', 'oni': '👹', 'yokai': '👻', 'kappa': '🐢', 'yurei': '👻',
'kami': '👼', 'shinigami': '💀', 'bakemono': '👹', 'tsukumogami': '🧸', 'noppera-bo': '👤', 'rokurokubi': '🧛', 'yuki-onna': '❄️', 'jorogumo': '🕷️', 'nue': '🐍', 'ubume': '👼',
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'rocket': '🚀', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
'comet': '☄️', 'constellation': '🌠', 'lightning': '', 'magnet': '🧲', 'battery': '🔋', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
'constellation': '🌠', 'lightning': '', 'magnet': '🧲', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
'cd': '💿', 'dvd': '📀', 'smartphone': '📱', 'tablet': '📲', 'watch': '', 'camera': '📷', 'video camera': '📹', 'projector': '📽️', 'radio': '📻', 'television': '📺',
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'flashlight': '🔦', 'circuit': '🔌', 'chip': '💻',
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'circuit': '🔌', 'chip': '💻',
'server': '🖥️', 'database': '💾', 'cloud': '☁️', 'network': '🌐', 'code': '💻', 'bug': '🐛', 'virus': '🦠', 'bacteria': '🦠', 'lab coat': '🥼', 'safety goggles': '🥽',
'test tube': '🧪', 'petri dish': '🧫', 'beaker': '🧪', 'bunsen burner': '🔥', 'graduated cylinder': '🧪', 'pipette': '🧪', 'scalpel': '🔪', 'syringe': '💉', 'pill': '💊',
'stethoscope': '🩺', 'thermometer': '🌡️', 'x-ray': '🩻', 'brain': '🧠', 'heart': '❤️', 'lung': '🫁', 'bone': '🦴', 'muscle': '💪', 'robot arm': '🦾', 'robot leg': '🦿',
@@ -124,21 +136,18 @@ def tableOfContents():
'spider': '🕷️', 'scorpion': '🦂', 'turkey': '🦃', 'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'sloth': '🦥', 'otter': '🦦',
'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'beaver': '🦫', 'bison': '🦬', 'mammoth': '🦣', 'raccoon': '🦝', 'hedgehog': '🦔', 'squirrel': '🐿️', 'chipmunk': '🐿️',
'porcupine': '🦔', 'llama': '🦙', 'giraffe': '🦒', 'zebra': '🦓', 'hippopotamus': '🦛', 'rhinoceros': '🦏', 'gorilla': '🦍', 'orangutan': '🦧', 'elephant': '🐘', 'camel': '🐫',
'llama': '🦙', 'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
'unicorn': '🦄', 'zebra': '🦓', 'cow': '🐄', 'pig': '🐖', 'boar': '🐗', 'mouse': '🐁', 'rat': '🐀', 'hamster': '🐹', 'rabbit': '🐇', 'chipmunk': '🐿️', 'beaver': '🦫', 'hedgehog': '🦔',
'bat': '🦇', 'bear': '🐻', 'koala': '🐨', 'panda': '🐼', 'sloth': '🦥', 'otter': '🦦', 'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'turkey': '🦃', 'chicken': '🐔', 'rooster': '🐓',
'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'snake': '🐍', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖',
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
'rat': '🐀', 'hedgehog': '🦔', 'chicken': '🐔', 'rooster': '🐓', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖', 'butterfly': '🦋',
'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'roll of paper': '🧻', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨‍🏫', 'referee': '🧑‍⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
return wordToEmojiMap
@@ -168,14 +177,14 @@ 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
return renderedLaugh
except Exception as e:
return random.choice(lameJokes)

View File

@@ -18,12 +18,12 @@ locale.setlocale(locale.LC_ALL, '')
lemon_starting_cash = 30.00
lemon_total_weeks = 7
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'last_played': 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,13 +50,14 @@ def getHighScoreLemon():
pickle.dump(high_score, file)
return high_score
def playLemonstand(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()
@@ -95,33 +96,6 @@ def playLemonstand(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"
@@ -240,22 +214,35 @@ def playLemonstand(nodeID, message, celsius=False):
if lemonadeScore[i]['nodeID'] == nodeID:
score.value = lemonadeScore[i]['value']
score.total = lemonadeScore[i]['total']
#handle last command
lemonsLastCmd = 'new'
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonsLastCmd = lemonadeTracker[i]['cmd']
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 lemonsLastCmd:
if newgame or "new" in lemonsLastCmd:
logger.debug("System: Lemonade: New Game: " + str(nodeID))
# 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"
# Create a new display buffer for the text messages
buffer= ""
@@ -272,7 +259,7 @@ def playLemonstand(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)
@@ -300,44 +287,39 @@ def playLemonstand(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 lemonsLastCmd:
if "cups" in lemonsLastCmd and not newgame:
# Read the number of cup boxes to purchase
newcups = -1
if "n" in message.lower():
@@ -351,22 +333,22 @@ def playLemonstand(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 lemonsLastCmd:
if "lemons" in lemonsLastCmd and not newgame:
# Read the number of lemon bags to purchase
newlemons = -1
if "n" in message.lower():
@@ -387,15 +369,15 @@ def playLemonstand(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 lemonsLastCmd:
if "sugar" in lemonsLastCmd and not newgame:
# Read the number of sugar bags to purchase
newsugar = -1
if "n" in message.lower():
@@ -415,8 +397,8 @@ def playLemonstand(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
@@ -426,14 +408,14 @@ def playLemonstand(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "price" in lemonsLastCmd:
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:
lemonsLastCmd = "sales"
@@ -456,7 +438,7 @@ def playLemonstand(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
if "sales" in lemonsLastCmd:
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)
@@ -486,7 +468,7 @@ def playLemonstand(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)
@@ -503,7 +485,7 @@ def playLemonstand(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. "
@@ -543,7 +525,7 @@ def playLemonstand(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
@@ -554,33 +536,31 @@ def playLemonstand(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]['last_played'] = time.time()
weeks.current = weeks.current + 1
msg += f"Play another week🥤? or (E)nd Game"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
else:

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'

View File

@@ -3,7 +3,9 @@
# 2025
from modules.log import *
import random
# to molly and jake, I miss you both so much.
import time
# to (max), molly and jake, I miss you both so much.
if disable_emojis_in_games:
X = "X"
@@ -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!"

313
modules/games/wodt.py Normal file
View File

@@ -0,0 +1,313 @@
# python word of the day game module for meshing-around bot
# 2025 K7MHI Kelly Keeton
from modules.log import *
import random
import json
import os
import time
from itertools import product
class WordOfTheDayGame:
def __init__(self):
self.bingo_board_size = 3 # 3x3 bingo board good for small demos
default_word_list = [
{'word': 'serendipity', 'meta': 'The occurrence of events by chance in a happy or beneficial way.'},
{'word': 'ephemeral', 'meta': 'Lasting for a very short time.'},
{'word': 'sonder', 'meta': 'The realization that each passerby has a life as vivid and complex as your own.'},
{'word': 'petrichor', 'meta': 'A pleasant smell that frequently accompanies the first rain after a long period of warm, dry weather.'},
]
json_path = os.path.join('data', 'wotd.json')
if os.path.exists(json_path):
try:
with open(json_path, 'r') as f:
self.word_list = json.load(f)
except FileNotFoundError:
logger.debug("System: WoTd: Failed to load data/wotd.json, using default word list.")
self.word_list = default_word_list
except json.JSONDecodeError:
logger.warning("System: WoTd: JSON decode error in data/wotd.json, example format: [{\"word\": \"example\", \"definition\": \"An example definition.\"}]")
self.word_list = default_word_list
else:
logger.debug("System: WoTd: data/wotd.json not found, using default word list.")
self.word_list = default_word_list
# Load bingo card words from JSON if available
default_bingo_card = [
"dog", "cat", "fish", "bird", "hamster", "rabbit", "turtle", "lizard", "snake", "frog",
"horse", "cow", "pig", "sheep", "goat", "chicken", "duck", "turkey", "peacock", "parrot",
"elephant", "lion", "tiger", "bear", "wolf", "fox", "deer", "moose", "zebra", "giraffe",
"monkey", "ape", "chimpanzee", "gorilla", "orangutan", "kangaroo", "koala", "panda",
"whale", "dolphin", "shark", "octopus", "crab", "lobster", "jellyfish", "seahorse",
"ant", "bee", "butterfly", "dragonfly", "spider", "ladybug"
]
bingo_json_path = os.path.join('data', 'bingo.json')
if os.path.exists(bingo_json_path):
try:
with open(bingo_json_path, 'r') as f:
bingoCard = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
logger.debug("System: WoTd: Failed to load data/bingo.json, using default bingo card. example format: [\"word1\", \"word2\", ...]")
bingoCard = default_bingo_card
else:
logger.debug("System: WoTd: data/bingo.json not found, using default bingo card.")
bingoCard = default_bingo_card
# Create a set for faster lookup
self.bingoCardSet = set(bingoCard)
self.leet_dict = {
'a': ['4', '@'],
'b': ['8'],
'e': ['3'],
'i': ['1', '!', '|'],
'l': ['1', '|', '7'],
'o': ['0'],
's': ['5', '$'],
't': ['7', '+'],
'g': ['9', '6'],
}
# Initialize the word of the day
self.word_of_the_day_entry = random.choice(self.word_list)
logger.debug(f"System: WoTd: Initialized with word of the day '{self.word_of_the_day_entry['word']}'.")
# Initialize bingo card
self.generate_bingo_card(self.bingo_board_size)
logger.debug("System: BINGO: " + ". ".join(" | ".join(row) for row in self.bingo_card))
def get_emoji_type(self, emoji, randomReturn=False):
smileys = "😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒🤕🤢🤮🤧😇🥳🥺🤠"
animals = "🐶🐱🐭🐹🐰🦊🐻🐼🐨🐯🦁🐮🐷🐽🐸🐵🙈🙉🙊🐒🐔🐧🐦🐤🐣🐥🦆🦅🦉🦇🐺🐗🐴🦄🐝🐛🦋🐌🐞🐜🦟🦗🕷️🕸️🐢🐍🦎🦂🦀🦞🦐🦑🐙🦑🐠🐟🐡🐬🦈🐳🐋🐊🐅🐆🦓🦍🦧🐘🦛🦏🐪🐫🦒🦘🐃🐂🐄🐎🐖🐏🐑🦙🐐🦌🐕🐩🦮🐕‍🦺🐈🐓🦃🦚🦜🦢🦩🕊️🐇🦝🦨🦡🦦🦥🐁🐀🐿️🦔"
fruit = "🍎🍊🍌🍉🍇🍓🍒🍑🥭🍍🥥🥝🍅🥑🍆🥔🥕🌽🌶️🥒🥬🥦🧄🧅🍄🥜🌰"
categories = {'smileys': smileys, 'animals': animals, 'fruit': fruit}
if randomReturn:
cat = random.choice(list(categories))
return random.choice(categories[cat])
for cat, chars in categories.items():
if emoji in chars:
return cat
return False
def reset_word_of_the_day(self):
logger.debug("System: WoTd: Resetting Word of the Day.")
self.word_of_the_day_entry = random.choice(self.word_list)
def generate_leet_variants(self, word):
chars = []
for c in word.lower():
if c in self.leet_dict:
chars.append([c] + self.leet_dict[c])
else:
chars.append([c])
variants = set()
for combo in product(*chars):
variant = ''.join(combo)
variants.add(variant)
if len(variants) > 128:
break
return variants
def did_it_happen(self, string_of_text=''):
"""
Check if the current word of the day (or its leet variants) appears in the text.
Also check for a bingo win.
Returns:
(wotd_found, old_entry, new_entry, bingo_win, bingo_message)
"""
text = string_of_text.lower()
words_in_text = set(text.split())
word = self.word_of_the_day_entry['word'].lower()
variants = self.generate_leet_variants(word)
wotd_found = False
old_entry = None
new_entry = None
for variant in variants:
if variant in words_in_text:
old_entry = self.word_of_the_day_entry
self.reset_word_of_the_day()
new_entry = self.word_of_the_day_entry
wotd_found = True
break
bingo_win, bingo_message = self.b_i_n_g_o(string_of_text)
return wotd_found, old_entry, new_entry, bingo_win, bingo_message
def generate_bingo_card(self, size=None):
"""
Generate a random bingo card of given size (size x size) from the bingoCardSet.
Returns a 2D list representing the bingo card.
"""
if size is None:
size = self.bingo_board_size
words = random.sample(list(self.bingoCardSet), size * size)
card = [words[i*size:(i+1)*size] for i in range(size)]
self.bingo_card = card
self.bingo_card_matches = [[False]*size for _ in range(size)]
return card
def b_i_n_g_o(self, string_of_text=''):
"""
Check if any words in the text match the bingo card.
If a row, column, or diagonal is fully matched, return True and the winning line.
Otherwise, return False and None.
"""
if not hasattr(self, 'bingo_card'):
logger.debug("System: WoTd: Generating new bingo card.")
self.generate_bingo_card(self.bingo_board_size)
words_in_text = set(string_of_text.lower().split())
size = len(self.bingo_card)
# Mark matches
for i in range(size):
for j in range(size):
if self.bingo_card[i][j].lower() in words_in_text:
self.bingo_card_matches[i][j] = True
# Check rows
for i in range(size):
if all(self.bingo_card_matches[i]):
winning_row = self.bingo_card[i]
logger.debug("System: BINGO achieved, generating new bingo card.")
self.generate_bingo_card(size) # Reset board after win
return True, f"BINGO! Row {i+1}: {winning_row}"
# Check columns
for j in range(size):
if all(self.bingo_card_matches[i][j] for i in range(size)):
col = [self.bingo_card[i][j] for i in range(size)]
logger.debug("System: BINGO achieved, generating new bingo card.")
self.generate_bingo_card(size) # Reset board after win
return True, f"BINGO! Column {j+1}: {col}"
# Check diagonals
if all(self.bingo_card_matches[i][i] for i in range(size)):
diag = [self.bingo_card[i][i] for i in range(size)]
logger.debug("System: BINGO achieved, generating new bingo card.")
self.generate_bingo_card(size) # Reset board after win
return True, f"BINGO! Diagonal: {diag}"
if all(self.bingo_card_matches[i][size-1-i] for i in range(size)):
diag = [self.bingo_card[i][size-1-i] for i in range(size)]
logger.debug("System: BINGO achieved, generating new bingo card.")
self.generate_bingo_card(size) # Reset board after win
return True, f"BINGO! Diagonal: {diag}"
return False, None
def extract_emojis(self, text):
emojis = []
for char in text:
cp = ord(char)
# Common emoji Unicode ranges
if (
0x1F600 <= cp <= 0x1F64F or # Emoticons
0x1F300 <= cp <= 0x1F5FF or # Symbols & pictographs
0x1F680 <= cp <= 0x1F6FF or # Transport & map symbols
0x1F1E6 <= cp <= 0x1F1FF or # Regional indicator symbols
0x2600 <= cp <= 0x26FF or # Misc symbols
0x2700 <= cp <= 0x27BF or # Dingbats
0x1F900 <= cp <= 0x1F9FF or # Supplemental symbols & pictographs
0x1FA70 <= cp <= 0x1FAFF or # Symbols & pictographs extended-A
0x2B50 == cp or # Star
0x2B55 == cp # Heavy large circle
):
emojis.append(char)
return emojis
def emojiMiniGame(self, string_of_text='', nodeID=0, nodeInt=1, emojiSeen=False):
from modules.system import meshLeaderboard
"""
Track emoji usage, Returns a string if the mini-game is won, else None.
If emojiSeen is False, only update mostMessages leaderboard and skip emoji logic.
"""
# Only increment for text/chat messages
meshLeaderboard['nodeMessageCounts'][nodeID] = meshLeaderboard['nodeMessageCounts'].get(nodeID, 0) + 1
# Update mostMessages leaderboard
if meshLeaderboard['nodeMessageCounts']:
max_node = max(meshLeaderboard['nodeMessageCounts'], key=meshLeaderboard['nodeMessageCounts'].get)
meshLeaderboard['mostMessages'] = {
'nodeID': max_node,
'value': meshLeaderboard['nodeMessageCounts'][max_node],
'timestamp': time.time()
}
emoji = None # Placeholder: extract emoji from string_of_text if needed
emojis = self.extract_emojis(string_of_text)
if not emojiSeen and not emojis:
return None
logger.debug(f"System: WoTd: Emoji mini-game processing for nodeID {nodeID} with emojis: {emojis}")
# --- 1. Update meshLeaderboard for emoji usage ---
if 'emojiCounts' not in meshLeaderboard:
meshLeaderboard['emojiCounts'] = {}
if 'emojiTypeCounts' not in meshLeaderboard:
meshLeaderboard['emojiTypeCounts'] = {}
meshLeaderboard['emojiCounts'].setdefault(nodeID, {})
for emoji in emojis:
meshLeaderboard['emojiCounts'][nodeID][emoji] = meshLeaderboard['emojiCounts'][nodeID].get(emoji, 0) + 1
# --- Update the leaderboard record for most emojis ---
# Flatten per-node emoji counts to total per node
emoji_totals = {nid: sum(emojicounts.values()) for nid, emojicounts in meshLeaderboard['emojiCounts'].items() if isinstance(emojicounts, dict)}
if emoji_totals:
max_node = max(emoji_totals, key=emoji_totals.get)
meshLeaderboard['mostEmojis'] = {
'nodeID': max_node,
'value': emoji_totals[max_node],
'timestamp': time.time()
}
# --- 2. Track most used of a type (e.g., smileys, animals, etc.) ---
emoji_type = self.get_emoji_type(emoji)
meshLeaderboard['emojiTypeCounts'].setdefault(emoji_type, {})
meshLeaderboard['emojiTypeCounts'][emoji_type][emoji] = meshLeaderboard['emojiTypeCounts'][emoji_type].get(emoji, 0) + 1
# --- 3. Slot machine mini-game ---
if 'emojiSlotWindow' not in meshLeaderboard:
meshLeaderboard['emojiSlotWindow'] = []
meshLeaderboard['emojiSlotWindow'].append(emoji)
# Randomize jackpot length after each win
if not hasattr(self, 'slot_jackpot_length'):
self.slot_jackpot_length = random.choice([3,4,5]) # JackPot length can be 3, 4, or 5
if len(meshLeaderboard['emojiSlotWindow']) > self.slot_jackpot_length:
meshLeaderboard['emojiSlotWindow'].pop(0)
# --- 3a. Detect spam of 3 identical emojis in a row ---
if len(meshLeaderboard['emojiSlotWindow']) >= 5:
last_three = meshLeaderboard['emojiSlotWindow'][-3:]
if len(set(last_three)) == 1:
# Option: Randomly add an emoji to break the spam
random_emoji = self.get_emoji_type('', randomReturn=True)
meshLeaderboard['emojiSlotWindow'].append(random_emoji)
logger.debug(f"System: WoTd: Detected emoji spam, added random emoji '{random_emoji}' to slot window.")
# Optionally, you can still scramble or pop as well if you want
random.shuffle(meshLeaderboard['emojiSlotWindow'])
meshLeaderboard['emojiSlotWindow'].pop()
# # Debug: Show slot window status before jackpot check
# logger.debug(
# f"Emoji Slot Window: {meshLeaderboard['emojiSlotWindow']} | "
# f"Jackpot Length: {self.slot_jackpot_length} | "
# f"Unique: {set(meshLeaderboard['emojiSlotWindow'])} | "
# f"Needed: {self.slot_jackpot_length - len(meshLeaderboard['emojiSlotWindow'])}"
# )
# Jackpot: all emojis in window are the same
if (
len(meshLeaderboard['emojiSlotWindow']) == self.slot_jackpot_length and
len(set(meshLeaderboard['emojiSlotWindow'])) == 1
):
winner_msg = f"🎰 JACKPOT! {emoji * self.slot_jackpot_length}"
meshLeaderboard['emojiSlotWindow'] = []
self.slot_jackpot_length = random.choice([3, 4, 5]) # Randomize jackpot length after win
return winner_msg
return None
# Example usage:
# theWordOfTheDay = WordOfTheDayGame()
# happened, entry = theWordOfTheDay.did_it_happen("I love serendipity!")
# if happened:
# print(f"Found the word of the day: {entry['word']} - {entry['meta']}")

View File

@@ -16,7 +16,7 @@ def get_govUK_alerts(lat, lon):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
soup = bs.BeautifulSoup(response.text, 'html.parser')
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
@@ -35,7 +35,7 @@ def get_nina_alerts():
alerts = []
for regionalKey in myRegionalKeysDE:
url = ("https://nina.api.proxy.bund.dev/api31/dashboard/" + regionalKey + ".json")
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
data = response.json()
for item in data:
@@ -53,7 +53,7 @@ def get_wxUKgov():
try:
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
soup = bs.BeautifulSoup(response.content, 'xml')
items = soup.find_all('item')

View File

@@ -8,6 +8,7 @@ from modules.log import *
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
import requests
import json
from datetime import datetime
if not rawLLMQuery:
# this may be removed in the future
@@ -48,7 +49,7 @@ meshBotAI = """
PROMPT
{input}
"""
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
@@ -76,6 +77,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 +222,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 +246,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 +272,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 +287,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

@@ -7,15 +7,19 @@ import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from datetime import datetime
from modules.log import *
import math
import csv
import os
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar")
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar", "map",)
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")
@@ -131,7 +135,7 @@ def getArtSciRepeaters(lat=0, lon=0):
if zipCode.isnumeric():
try:
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
response = requests.get(artsci_url)
response = requests.get(artsci_url, timeout=urlTimeoutSeconds)
if response.status_code!=200:
logger.error(f"Location:Error fetching data from {artsci_url} with status code {response.status_code}")
soup = bs.BeautifulSoup(response.text, 'html.parser')
@@ -171,9 +175,10 @@ 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
lat = latitudeValue
lon = longitudeValue
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
try:
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
@@ -235,8 +240,10 @@ 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
lat = latitudeValue
lon = longitudeValue
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
@@ -292,9 +299,37 @@ 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",
"gusts as high as": "gusts to",
}
# Single words (no spaces)
word_replacements = {
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
@@ -310,6 +345,8 @@ def abbreviate_noaa(row):
"south": "S",
"east": "E",
"west": "W",
"accumulation": "accum",
"visibility": "vis",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms",
@@ -331,27 +368,37 @@ 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",
"around": "~",
"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 useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -422,9 +469,10 @@ 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
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -484,10 +532,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
except Exception as e:
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
return ERROR_FETCHING_DATA
# main feed bulletins
@@ -569,14 +617,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# check if the alert is for the SAME location, if wanted keep alert
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
# ignore the FEMA test alerts
ignore_alert = False
if ignoreFEMAenable:
ignore_alert = False
for word in ignoreFEMAwords:
if word.lower() in headline.lower():
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
ignore_alert = True
break
ignore_alert = any(
word.lower() in headline.lower()
for word in ignoreFEMAwords)
if ignore_alert:
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
if ignore_alert:
continue
@@ -701,7 +748,7 @@ def get_volcano_usgs(lat=0, lon=0):
return alerts
def get_nws_marine(zone, days=3):
# forcast from NWS coastal products
# forecast from NWS coastal products
try:
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
if not marine_pz_data.ok:
@@ -710,18 +757,21 @@ def get_nws_marine(zone, days=3):
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching NWS Marine PZ data")
return ERROR_FETCHING_DATA
marine_pz_data = marine_pz_data.text
#validate data
todayDate = today.strftime("%Y%m%d")
if marine_pz_data.startswith("Expires:"):
expires = marine_pz_data.split(";;")[0].split(":")[1]
expires_date = expires[:8]
if expires_date < todayDate:
logger.debug("Location: NWS Marine PZ data expired")
todayDate = datetime.now().strftime("%Y%m%d")
if marine_pz_data and marine_pz_data.startswith("Expires:"):
try:
expires = marine_pz_data.split(";;")[0].split(":")[1]
expires_date = expires[:8]
if expires_date < todayDate:
logger.debug("Location: NWS Marine PZ data expired")
return ERROR_FETCHING_DATA
except Exception as e:
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
return ERROR_FETCHING_DATA
else:
logger.debug("Location: NWS Marine PZ data not valid")
logger.debug("Location: NWS Marine PZ data not valid or empty")
return ERROR_FETCHING_DATA
# process the marine forecast data
@@ -813,6 +863,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:
@@ -1005,3 +1056,89 @@ def get_openskynetwork(lat=0, lon=0):
aircraft_report = aircraft_report[:-1]
aircraft_report = abbreviate_noaa(aircraft_report)
return aircraft_report if aircraft_report else NO_ALERTS
def log_locationData_toMap(userID, location, message):
"""
Logs location data to a CSV file for meshing purposes.
Returns True if successful, False otherwise.
"""
lat, lon = location
if lat is None or lon is None or lat == 0.0 or lon == 0.0:
return False
# Set default directory to ../data/
default_dir = os.path.join(os.path.dirname(__file__), "..", "data")
os.makedirs(default_dir, exist_ok=True)
map_filepath = os.path.join(default_dir, "map_data.csv")
# Check if the file exists to determine if we need to write headers
write_header = not os.path.isfile(map_filepath) or os.path.getsize(map_filepath) == 0
try:
with open(map_filepath, mode='a', newline='') as csvfile:
fieldnames = ['userID', 'Latitude', 'Longitude', 'Description']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers only if the file did not exist before or is empty
if write_header:
writer.writeheader()
writer.writerow({
'userID': userID,
'Latitude': lat,
'Longitude': lon,
'Description': message if message else ""
})
logger.debug(f"Logged location for {userID} to {map_filepath}")
return True
except Exception as e:
logger.error(f"Failed to log location for {userID}: {e}")
return False
def mapHandler(userID, deviceID, channel_number, message, snr, rssi, hop):
from modules.system import get_node_location
command = message[len("map"):].strip()
location = get_node_location(userID, deviceID)
lat = location[0]
lon = location[1]
"""
Handles 'map' commands from meshbot.
Usage:
map <description text> - Log current location with description
"""
command = str(command) # Ensure command is always a string
if command.strip().lower() == "?":
return (
"Usage:\n"
" 🗺map <description text> - Log your current location with a description\n"
"Example:\n"
" 🗺map Found a new mesh node near the park"
)
description = command.strip()
# if no description provided, set to default
if not description:
description = "Logged:"
# Sanitize description for CSV injection
if description and description[0] in ('=', '+', '-', '@'):
description = "'" + description
# if there is SNR and RSSI info, append to description
if snr is not None and rssi is not None:
description += f" SNR:{snr}dB RSSI:{rssi}dBm"
# if there is hop info, append to description
if hop is not None:
description += f" Meta:{hop}"
# location should be a tuple: (lat, lon)
if not location or len(location) != 2:
return "🚫Location data is missing or invalid."
success = log_locationData_toMap(userID, location, description)
if success:
return f"📍Location logged "
else:
return "🚫Failed to log location. Please try again."

View File

@@ -1,7 +1,5 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
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:
@@ -38,11 +36,17 @@ class CustomFormatter(logging.Formatter):
return formatter.format(record)
class plainFormatter(logging.Formatter):
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
ansi_codes = [
'\x1b[38;21m', '\x1b[38;5;231m', '\x1b[38;5;39m', '\x1b[38;5;226m',
'\x1b[38;5;196m', '\x1b[38;5;46m', '\x1b[38;5;129m', '\x1b[31;1m',
'\x1b[37;1m', '\x1b[0m'
]
def format(self, record):
message = super().format(record)
return self.ansi_escape.sub('', message)
for code in self.ansi_codes:
message = message.replace(code, '')
return message
# Create logger
logger = logging.getLogger("MeshBot System Logger")
@@ -56,7 +60,6 @@ msgLogger.propagate = False
# Define format for logs
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
msgLogFormat = '%(asctime)s | %(message)s'
today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()

View File

@@ -2,23 +2,46 @@
# 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
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
# 2024 Kelly Keeton K7MHI
previousVoxState = False
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()
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")
@@ -27,8 +50,56 @@ if voxDetectionEnabled:
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)
@@ -50,115 +121,46 @@ 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 vox_callback(indata, frames, time, status):
if status:
logger.warning(f"RadioMon: VOX input status: {status}")
q.put(bytes(indata))
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
@@ -184,31 +186,42 @@ async def signalWatcher():
signalCycle = 0
previousStrength = -40
def make_vox_callback(loop, q):
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
voxInputDevice = None
async def voxMonitor():
global previousVoxState, voxMsgQueue
try:
model = Model(lang="en-us")
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}")
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 = make_vox_callback(loop, q)
callback = await make_vox_callback(loop, q)
with sd.RawInputStream(
device=voxInputDevice,
samplerate=samplerate,
blocksize=8000,
blocksize=4000,
dtype='int16',
channels=1,
callback=callback
@@ -218,11 +231,16 @@ async def voxMonitor():
if rec.AcceptWaveform(data):
result = rec.Result()
text = json.loads(result).get("text", "")
if text and text != "huh":
logger.info(f"🎙Detected {voxDescription}: {text}")
voxMsgQueue.append(f"🎙Detected {voxDescription}: {text}")
await asyncio.sleep(0.5)
# 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
# end of file

View File

@@ -4,6 +4,7 @@ import urllib.request
import xml.etree.ElementTree as ET
import html
from html.parser import HTMLParser
import bs4 as bs
class MLStripper(HTMLParser):
def __init__(self):
@@ -16,9 +17,12 @@ class MLStripper(HTMLParser):
return ''.join(self.fed)
def strip_tags(html_text):
s = MLStripper()
s.feed(html_text)
return s.get_data()
# use BeautifulSoup to strip HTML tags
if not html_text:
return ""
soup = bs.BeautifulSoup(html_text, "html.parser")
text = soup.get_text(separator=" ", strip=True)
return ' '.join(text.split())
RSS_FEED_URLS = rssFeedURL
RSS_FEED_NAMES = rssFeedNames

83
modules/scheduler.py Normal file
View File

@@ -0,0 +1,83 @@
# modules/scheduler.py 2025 meshing-around
# Scheduler setup for Mesh Bot
import asyncio
import schedule
from modules.log import logger
from modules.system import send_message
async def setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
# methods available for custom scheduler messages
from mesh_bot import tell_joke, welcome_message, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
schedulerValue = schedulerValue.lower().strip()
schedulerTime = schedulerTime.strip()
schedulerInterval = schedulerInterval.strip()
schedulerChannel = int(schedulerChannel)
schedulerInterface = int(schedulerInterface)
# Setup the scheduler based on configuration
try:
if schedulerMotd:
scheduler_message = MOTD
else:
scheduler_message = schedulerMessage
# Basic Scheduler Options
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
# Basic scheduler job to run the schedule see examples below for custom schedules
if schedulerValue.lower() == 'day':
if schedulerTime != '':
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
else:
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'hour' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'min' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'joke' in schedulerValue.lower():
# Schedule to send a joke every specified interval
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'link' in schedulerValue.lower():
# Schedule to send a link message every specified interval
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_satpass(schedulerInterface, 'link'), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the link scheduler to send link messages every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'weather' in schedulerValue.lower():
# Schedule to send weather updates every specified interval
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'custom' in schedulerValue.lower():
# Import and setup custom schedules from custom_scheduler.py
try:
# This file is located in etc/custom_scheduler.py and copied to modules/custom_scheduler.py at install
from modules.custom_scheduler import setup_custom_schedules # type: ignore # pylance
setup_custom_schedules(
send_message, tell_joke, welcome_message, handle_wxc, MOTD,
schedulerChannel, schedulerInterface)
logger.debug("System: Custom scheduler file imported and custom schedules set up.")
except Exception as e:
logger.debug(f"System: Failed to import custom scheduler. {e}")
logger.warning("Custom scheduler file not found or failed to import. cp etc/custom_scheduler.py modules/custom_scheduler.py")
# Start the Broadcast Scheduler
await BroadcastScheduler()
except Exception as e:
logger.error(f"System: Scheduler Error {e}")

View File

@@ -28,11 +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
surveyTracker, tictactoeTracker, hamtestTracker, hangmanTracker, golfTracker, mastermindTracker, vpTracker, blackjackTracker, lemonadeTracker, dwPlayerTracker, jackTracker = [], [], [], [], [], [], [], [], [], [], [] # game trackers
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()
@@ -259,6 +271,7 @@ try:
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentryWatchList = config['sentry'].get('sentryWatchList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
@@ -268,11 +281,17 @@ 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
cmdShellSentryAlerts = config['sentry'].getboolean('cmdShellSentryAlerts', False) # default False
sentryAlertNear = config['sentry'].get('sentryAlertNear', 'sentry_alert_near.sh') # default sentry_alert_near.sh
sentryAlertFar = config['sentry'].get('sentryAlertFar', 'sentry_alert_far.sh') # default sentry_alert_far.sh
# 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
@@ -368,6 +387,13 @@ try:
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)
@@ -378,7 +404,7 @@ try:
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', False) # default False
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', True) # default True
xCmd2factor_timeout = config['fileMon'].getint('2factor_timeout', 100) # default 100 seconds
# games
@@ -395,15 +421,17 @@ try:
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)
wordOfTheDay = config['games'].getboolean('wordOfTheDay', 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 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
@@ -413,6 +441,6 @@ try:
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
except Exception as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")
print("System: Check the config.ini against config.template file for missing sections or values.")
print("System: Exiting...")
exit(1)

View File

@@ -10,6 +10,8 @@
import json
import os # For file operations
import csv
from datetime import datetime
from collections import Counter
from modules.log import *
@@ -49,20 +51,25 @@ class SurveyModule:
logger.error(f"Survey: Error loading surveys: {e}")
def start_survey(self, user_id, survey_name='example', location=None):
"""Begin a new survey session for a user."""
if not survey_name:
survey_name = 'example'
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
try:
"""Begin a new survey session for a user."""
if not survey_name:
survey_name = default_survey
if survey_name not in allowedSurveys:
return f"error: survey '{survey_name}' is not allowed."
self.responses[user_id] = {
'survey_name': survey_name,
'current_question': 0,
'answers': [],
'location': location if surveyRecordLocation and location is not None else 'N/A'
}
msg = f"'{survey_name}'📝survey\n"
msg += self.show_question(user_id)
msg += f"\nSend answer' or 'end'"
return msg
except Exception as e:
logger.error(f"Error starting survey for user {user_id}: {e}")
return "An error occurred while starting the survey. Please try again later."
def show_question(self, user_id):
"""Show the current question for the user, or end the survey."""
@@ -91,18 +98,99 @@ class SurveyModule:
return
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
try:
# Check if file exists and if it has a header
write_header = not os.path.isfile(filename) or os.path.getsize(filename) == 0
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")
# Write header if needed
if write_header:
header = ['timestamp', 'user_id', 'location'] + [f'Q{i+1}' for i in range(len(self.responses[user_id]['answers']))]
f.write(','.join(header) + '\n')
# Always write: timestamp, userID, position, answers...
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
user_id_str = str(user_id)
location = self.responses[user_id].get('location', "N/A")
answers = list(map(str, self.responses[user_id]['answers']))
row = [timestamp, user_id_str, str(location)] + answers
f.write(','.join(row) + '\n')
logger.info(f"Survey: Responses for user {user_id} saved for survey '{survey_name}' to {filename}.")
except Exception as e:
logger.error(f"Error saving responses to {filename}: {e}")
def format_survey_results(self, results):
if isinstance(results, dict) and "error" in results:
return results["error"]
if not results:
return "No results found."
msg = "📊Survey Results:\n"
for idx, q in enumerate(results):
msg += f"\nQ{idx+1}: {q['question']}\n"
if q['type'] == 'multiple_choice':
for opt, count in q['summary'].items():
msg += f" {opt}: {count}\n"
elif q['type'] == 'integer':
s = q['summary']
msg += f" Count: {s['count']}, Avg: {s['average']:.2f}, Min: {s['min']}, Max: {s['max']}\n"
elif q['type'] == 'text':
msg += f" Responses: {q['summary']['responses_count']}\n"
return msg
def get_survey_results(self, survey_name='example'):
if survey_name not in self.surveys:
return {"error": f"Survey '{survey_name}' not found."}
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
questions = self.surveys[survey_name]
results = []
try:
with open(filename, encoding='utf-8') as f:
reader = csv.reader(f)
lines = []
for row in reader:
if not row or len(row) < 4:
continue
# If location field is split due to comma, join columns 2 and 3
if row[2].startswith('[') and not row[2].endswith(']') and len(row) > 4:
location = row[2] + ',' + row[3]
answers = row[4:]
else:
location = row[2]
answers = row[3:]
lines.append(answers)
for q_idx, question in enumerate(questions):
qtype = question.get('type', 'multiple_choice')
answers = [row[q_idx] for row in lines if len(row) > q_idx]
summary = {}
if qtype == 'multiple_choice':
counts = Counter(answers)
summary = {chr(65+i): counts.get(chr(65+i), 0) for i in range(len(question.get('options', [])))}
elif qtype == 'integer':
ints = [int(a) for a in answers if a.isdigit()]
summary = {
"count": len(ints),
"average": sum(ints)/len(ints) if ints else 0,
"min": min(ints) if ints else None,
"max": max(ints) if ints else None
}
elif qtype == 'text':
summary = {"responses_count": len([a for a in answers if a.strip()])}
results.append({
"question": question['question'],
"type": qtype,
"summary": summary
})
return results
except FileNotFoundError:
return {"error": f"No responses recorded yet for '{survey_name}'."}
except Exception as e:
logger.error(f"Error summarizing survey results: {e}")
return NO_ALERTS
def answer(self, user_id, answer, location=None):
try:
"""Record an answer and return the next question or end message."""
@@ -121,7 +209,8 @@ class SurveyModule:
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))
# Valid answer record letter, not index
self.responses[user_id]['answers'].append(answer_char)
self.responses[user_id]['current_question'] += 1
return f"Recorded..\n" + self.show_question(user_id)
else:

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

View File

@@ -6,9 +6,8 @@ import wikipedia # pip install wikipedia
# Kiwix support for local wiki
if use_kiwix_server:
import requests
from bs4 import BeautifulSoup
import bs4 as bs
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:
@@ -16,13 +15,13 @@ if wikipedia_enabled and use_kiwix_server:
"""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):
if isinstance(element, bs.element.Comment):
return False
return True
def text_from_html(body):
"""Extract visible text from HTML content"""
soup = BeautifulSoup(body, 'html.parser')
soup = bs.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())

View File

@@ -10,6 +10,7 @@ except ImportError:
import asyncio
import time # for sleep, get some when you can :)
from datetime import datetime
import random
from modules.log import *
from modules.system import *
@@ -50,9 +51,6 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
logger.debug(f"System: Bot detected Commands:{cmds}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
@@ -72,30 +70,41 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
type = ''
if "ping" in message.lower():
msg = "🏓PONG\n"
msg = "🏓PONG"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
"🎙Testing, testing\n",\
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
"🎙Roger that!\n",])
msg = random.choice(["🎙Testing 1,2,3", "🎙Testing",\
"🎙Testing, testing",\
"🎙Ah-wun, ah-two...", "🎙Is this thing on?",\
"🎙Roger that!",])
type = "🎙TEST"
elif "ack" in message.lower():
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
myname = get_name_from_number(deviceID, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
myname = get_name_from_number(deviceID, 'short', 2)
msg = f"QSP QSL OM DE {myname} K\n"
else:
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
# append SNR/RSSI or hop info
if hop.startswith("Gateway") or hop.startswith("MQTT"):
msg += " [GW]"
elif hop.startswith("Direct"):
msg += " [RF]"
else:
msg = msg + hop
#flood
msg += " [F]"
if (float(snr) != 0 or float(rssi) != 0) and "Hops" not in hop:
msg += f"\nSNR:{snr} RSSI:{rssi}"
elif "Hops" in hop:
msg += f"\n{hop}🐇 "
else:
msg += "\nflood route"
if "@" in message:
msg = msg + " @" + message.split("@")[1]
@@ -150,7 +159,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
def handle_motd(message, message_from_id, isDM):
global MOTD
isAdmin = False
msg = ""
msg = MOTD
# check if the message_from_id is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
@@ -216,11 +225,16 @@ def onReceive(packet, interface):
rxType = type(interface).__name__
# Valies assinged to the packet
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
replyIDset = False
rxNodeHostName = None
emojiSeen = False
simulator_flag = False
isDM = False
channel_name = "unknown"
session_passkey = None
playingGame = False
if DEBUGpacket:
# Debug print the interface object
@@ -229,45 +243,60 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
# determine the rxNode based on the interface type
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
rxNodeHostName = interface.__dict__.get('ip', None)
rxNode = next(
(i for i in range(1, 10)
if multiple_interface and rxHost and
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
globals().get(f'interface{i}_type', '') == 'tcp'),None)
if rxType == 'BLEInterface':
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'port{i}', '') in rxInterface),None)
# check if the packet has a channel flag use it
if rxType == 'BLEInterface':
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'interface{i}_type', '') == 'ble'),0)
if rxNode is None:
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
rxNode = 1
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
if packet.get('channel'):
channel_number = packet.get('channel', 0)
channel_number = packet.get('channel')
# get channel name from channel number from connected devices
for device in channel_list:
if device["interface_id"] == rxNode:
device_channels = device['channels']
for chan_name, info in device_channels.items():
if info['number'] == channel_number:
channel_name = chan_name
break
# get channel hashes for the interface
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
if device:
# Find the channel name whose hash matches channel_number
for chan_name, info in device['channels'].items():
if info['hash'] == channel_number:
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
channel_name = chan_name
break
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
@@ -282,10 +311,11 @@ def onReceive(packet, interface):
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
via_mqtt = packet['decoded'].get('viaMqtt', False)
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay detected")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
@@ -294,53 +324,69 @@ def onReceive(packet, interface):
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has replyId flag // currently unused in the code
if packet.get('replyId'):
replyIDset = packet.get('replyId', False)
# check if the packet has emoji flag set it // currently unused in the code
if packet.get('emoji'):
emojiSeen = packet.get('emoji', False)
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet.get('hopsAway', 0)
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
# calculate hop count
hop = ""
if hop_limit > 0 and hop_start >= hop_limit:
hop_count = hop_away + (hop_start - hop_limit)
elif hop_limit > 0 and hop_start < hop_limit:
hop_count = hop_away + (hop_limit - hop_start)
else:
# if the packet does not have a hop count try other methods
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
hop_count = hop_away
if hop == "" and hop_count > 0:
# set hop string from calculated hop count
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower() and (snr != 0 or rssi != 0):
# 2.7+ firmware direct hop over LoRa
hop = "Direct"
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
elif hop == "" and hop_count == 0 and (snr != 0 or rssi != 0):
# this came from a UDP but we had signal info so gateway is used
hop = "Gateway"
elif "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
# we for sure detected this sourced from a UDP like host
hop = "Gateway"
if hop in ("MQTT", "Gateway") and hop_count > 0:
hop = f"{hop_count} Hops"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
hop_count = 0
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
hop = "MQTT"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
hop_count = hop_away
else:
hop_count = hop_start - hop_limit
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
hop = f"{hop_count} hops"
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
@@ -353,7 +399,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:
@@ -422,7 +467,7 @@ def onReceive(packet, interface):
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
print (CustomFormatter.bold_white + "\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')

60
script/README.md Normal file
View File

@@ -0,0 +1,60 @@
## script/runShell.sh
**Purpose:**
`runShell.sh` is a simple demo shell script for the Mesh Bot project. It demonstrates how to execute shell commands within the projects scripting environment.
**Usage:**
Run this script from the terminal to see a basic example of shell scripting in the project context.
```sh
bash script/runShell.sh
```
**What it does:**
- Changes the working directory to the scripts location.
- Prints the current directory path and a message indicating the script is running.
- Serves as a template for creating additional shell scripts or automating tasks related to the project.
**Note:**
You can modify this script to add more shell commands or automation steps as needed for your workflow.
## script/runShell.sh
**Purpose:**
`runShell.sh` is a demonstration shell script for the Mesh Bot project, showing how to execute shell commands in the project environment.
**Usage:**
Run this script from the terminal to see a basic shell scripting example:
```sh
bash script/runShell.sh
```
**What it does:**
- Changes to the scripts directory.
- Prints the current directory path.
- Displays a message indicating the script is running.
**Note:**
You can use this as a template for your own shell scripts or to automate project-related tasks.
## script/sysEnv.sh
**Purpose:**
`sysEnv.sh` is a shell script that collects and displays system telemetry and environment information, especially useful for monitoring a Raspberry Pi or similar device running the Mesh Bot.
**Usage:**
Run this script from the terminal to view system stats and network info:
```sh
bash script/sysEnv.sh
```
**What it does:**
- Reports disk space, RAM usage, CPU usage, and CPU temperature (in °C and °F).
- Checks for available Git updates if the project is a Git repository.
- Displays the devices public and local IP addresses.
- Designed to work on Linux systems, with special handling for Raspberry Pi hardware.
**Note:**
You can expand or modify this script to include additional telemetry or environment checks as needed for your deployment.

View File

@@ -28,6 +28,7 @@ if __name__ == "__main__":
# 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:
@@ -93,7 +94,7 @@ try:
count_devices = set([fav['deviceID'] for fav in favList])
count_nodes = set([fav['nodeID'] for fav in favList])
for fav in favList:
print(f"Device: {fav.get('deviceID', 'N/A')} Node: {fav.get('nodeID', 'N/A')} Interface: {fav.get('interface', 'N/A')}")
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.")
@@ -109,8 +110,9 @@ 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:

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

@@ -1,52 +1,51 @@
# OLD Docker Compose configuration for Meshing Around application with optional services.
# This setup includes the main Meshing Around service, with optional Ollama and Prometheus Node Exporter services.
# Adjust device mappings, ports, and configurations as needed for your environment.
configs:
me_config:
file: ./config.ini # Path to the configuration file for Meshing Around.
# Windows users may need to adjust the path format, e.g., C:/path/to/config.ini
services:
meshing-around:
build:
context: ../..
depends_on:
ollama:
condition: service_healthy
devices:
- /dev/ttyAMA10 # Replace this with your actual device!
configs:
- source: me_config
target: /app/config.ini
devices: # Optional if using meshtasticd. Pass through radio device.
- /dev/ttyAMA10 # Replace this with your actual device!
#- /dev/ttyUSB0 # Example for USB device
user: 1000:1000 # run as non-root user for better security.
extra_hosts:
- "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp
ollama:
image: ollama/ollama:0.5.1
volumes:
- ./ollama:/root/.ollama
- ./ollama-entrypoint.sh:./entrypoint.sh
container_name: ollama
pull_policy: always
tty: true
restart: always
entrypoint:
- /usr/bin/bash
- /script/docker/entrypoint.sh
expose:
- 11434
healthcheck:
test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
interval: 30s
timeout: 10s
retries: 20
node-exporter:
image: quay.io/prometheus/node-exporter:latest
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- --path.procfs=/host/proc
- --path.rootfs=/rootfs
- --path.sysfs=/host/sys
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
# - "host.docker.internal:host-gateway" # Enables access to host services from within the container.
container_name: meshing-around
restart: unless-stopped
expose:
- 9100
network_mode: host
pid: host
configs:
me_config:
file: ./config.ini
#tty: true # Enable only if interactive terminal is needed.
ports:
#- "8420:8420" # web report interface
#- "443:443" # HTTPS interface meshtasticD
environment:
- OLLAMA_API_URL=http://ollama:11434
# Uncomment the following service if you want to enable Ollama for local LLM API access.
# ollama:
# image: ollama/ollama:0.5.1
# volumes:
# - ./ollama:/root/.ollama
# - ./ollama-entrypoint.sh:./entrypoint.sh
# container_name: ollama
# pull_policy: always
# tty: true
# restart: always
# entrypoint:
# - /usr/bin/bash
# - /script/docker/entrypoint.sh
# ports:
# - "11434:11434"
# healthcheck:
# test: "curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
# interval: 30s
# timeout: 10s
# retries: 20

View File

@@ -8,8 +8,8 @@ pid=$!
# Pause for Ollama to start.
sleep 5
echo "🔴 Retrieve llama3.2:3b model..."
ollama pull llama3.2:3b
echo "🔴 Retrieve gemma3:270m model..."
ollama pull gemma3:270m
echo "🟢 Done!"
# Wait for Ollama process to finish.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Usage: python3 script/injectDM.py -s NODEID -d NODEID -m "message"
# meshing-around - helper script
# meshing-around - helper script - enable the bbsAPI in config.ini first
import sys
import os
import argparse

View File

@@ -42,3 +42,15 @@ then
fi
fi
fi
# Get public and local IP addresses
public_ip=$(curl -s https://ifconfig.me 2>/dev/null)
public_ip=${public_ip:-""}
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
local_ip=${local_ip:-""}
if [ -n "$public_ip" ]; then
echo "Public IP: $public_ip"
fi
if [ -n "$local_ip" ]; then
echo "Local IP: $local_ip"
fi

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,26 +44,45 @@ 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"
# copy modules/custom_scheduler.py template if it does not exist
if [[ ! -f modules/custom_scheduler.py ]]; then
cp -n etc/custom_scheduler.py modules/
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
fi
# Backup the data/ directory
echo "Backing up data/ directory..."
#backup_file="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
backup_file="data_backup.tar.gz"
path2backup="data/"
#copy custom_scheduler.py if it exists
if [[ -f "modules/custom_scheduler.py" ]]; then
echo "Including custom_scheduler.py in backup..."
cp modules/custom_scheduler.py data/
fi
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
if [ "$service_stopped" = true ]; then
if [[ "$service_stopped" = true ]]; then
echo "Restarting services..."
systemctl start mesh_bot.service
systemctl start pong_bot.service