Compare commits

...

2227 Commits

Author SHA1 Message Date
SpudGunMan
32b60297c8 Update README.md 2025-10-30 17:00:01 -07:00
SpudGunMan
f15a871967 fix alerting 2025-10-30 16:59:55 -07:00
SpudGunMan
a346354dbc add reporting service back in
let me know if errors
2025-10-30 13:00:44 -07:00
SpudGunMan
3d8007bbf6 docs 2025-10-30 10:32:35 -07:00
SpudGunMan
bb254474d0 fix config.ini
ownership issues my fault for not having this done a long time ago
2025-10-30 10:23:46 -07:00
SpudGunMan
37e3790ee4 Update update.sh 2025-10-30 09:03:19 -07:00
SpudGunMan
0ec380931a Update README.md 2025-10-30 07:42:46 -07:00
SpudGunMan
9cfd1bc670 Update README.md 2025-10-30 07:39:12 -07:00
SpudGunMan
a672c94303 Update README.md 2025-10-30 07:38:32 -07:00
SpudGunMan
92b3574c22 news sort by 2025-10-30 07:24:54 -07:00
SpudGunMan
27d8e198ae Update rss.py 2025-10-30 05:52:52 -07:00
SpudGunMan
11eeaa445a Update rss.py 2025-10-30 05:51:20 -07:00
SpudGunMan
57efc8a69b more
🐄🫑
2025-10-30 00:16:49 -07:00
SpudGunMan
7442ce11b4 Update rss.py 2025-10-29 23:58:47 -07:00
SpudGunMan
8bb6ba4d8e Update rss.py 2025-10-29 23:48:24 -07:00
SpudGunMan
da10af8d93 Update rss.py 2025-10-29 23:37:18 -07:00
SpudGunMan
46a33178f6 Update rss.py 2025-10-29 23:35:12 -07:00
SpudGunMan
e07c5a923e headline
headline command which uses NewsAPI.org
2025-10-29 23:28:14 -07:00
SpudGunMan
d330f3e0d6 patchAlerts 2025-10-29 21:51:57 -07:00
SpudGunMan
eddb2fe08c patch alerting 2025-10-29 21:49:14 -07:00
SpudGunMan
ebe729cf13 leaderboardFix 2025-10-29 21:36:05 -07:00
SpudGunMan
41a45c6e9c Update README.md 2025-10-29 21:22:15 -07:00
SpudGunMan
4224579f79 Update checklist.md 2025-10-29 21:16:32 -07:00
SpudGunMan
aa43d4acad auto Approve
approval is needed to alarm
2025-10-29 21:12:54 -07:00
SpudGunMan
4406f2b86f it SPEAKS
KittenML/KittenTTS
2025-10-29 20:52:14 -07:00
SpudGunMan
649c959304 Update radio.py 2025-10-29 19:28:41 -07:00
SpudGunMan
3529e40743 Update radio.py 2025-10-29 19:05:00 -07:00
SpudGunMan
f5c2dfa5e4 cleanup 2025-10-29 12:19:09 -07:00
SpudGunMan
1fb144ae1e docs 2025-10-29 11:43:33 -07:00
SpudGunMan
7e66ffc3a0 docs 2025-10-29 11:42:48 -07:00
SpudGunMan
d7371fae98 change approve 2025-10-29 11:39:47 -07:00
SpudGunMan
e4c51c97a1 Update checklist.py 2025-10-29 11:37:19 -07:00
SpudGunMan
70f072d222 default is nonApproved 2025-10-29 11:29:33 -07:00
SpudGunMan
8bb587cc7a Update checklist.py 2025-10-29 11:22:32 -07:00
SpudGunMan
313c313412 cleanup admin checklist 2025-10-29 11:15:02 -07:00
SpudGunMan
e5e8fbd0b5 Update checklist.py 2025-10-29 10:55:44 -07:00
SpudGunMan
2ef96f3ae3 Update checklist.py 2025-10-29 10:52:30 -07:00
SpudGunMan
a58605aba3 Update system.py 2025-10-29 10:47:29 -07:00
SpudGunMan
ffdd3a1ea9 enhance 2025-10-29 10:44:42 -07:00
SpudGunMan
185de28139 Update system.py 2025-10-29 10:30:26 -07:00
SpudGunMan
0eea36fba2 Update system.py 2025-10-29 10:16:20 -07:00
SpudGunMan
cb9e62894d Update system.py 2025-10-29 10:11:11 -07:00
SpudGunMan
9443d5fb0a Update system.py 2025-10-29 10:06:53 -07:00
SpudGunMan
1751648b12 Update system.py 2025-10-29 10:03:26 -07:00
SpudGunMan
8823d415c3 Update mesh_bot.py 2025-10-29 09:58:44 -07:00
SpudGunMan
55a1d951a7 Update mesh_bot.py 2025-10-29 09:57:42 -07:00
SpudGunMan
c8096107a0 Update mesh_bot.py 2025-10-29 09:55:56 -07:00
SpudGunMan
5bdf1a9d6c Update mesh_bot.py 2025-10-29 09:54:56 -07:00
SpudGunMan
85344db27e Update settings.py 2025-10-29 09:50:51 -07:00
SpudGunMan
5990a859d9 Update settings.py 2025-10-29 09:49:23 -07:00
SpudGunMan
ad6a55b9cd Update checklist.py 2025-10-29 09:47:50 -07:00
SpudGunMan
6fcd981eae Update mesh_bot.py 2025-10-29 09:47:11 -07:00
SpudGunMan
9564c92cc8 Update checklist.py 2025-10-29 09:46:08 -07:00
SpudGunMan
149dc10df6 Create test_checklist.py 2025-10-29 09:44:27 -07:00
SpudGunMan
e211efca4e Update mesh_bot.py 2025-10-29 09:35:37 -07:00
SpudGunMan
a974de790b refactor Alerts 2025-10-29 09:31:21 -07:00
SpudGunMan
777c423f17 refactor 2025-10-29 09:30:59 -07:00
SpudGunMan
dbcb93eabb refactor Alerts 🚨 2025-10-29 08:29:20 -07:00
SpudGunMan
69518ea317 enhance 2025-10-29 08:21:09 -07:00
SpudGunMan
11faea2b4e purge 2025-10-29 00:48:59 -07:00
SpudGunMan
acb0e870d6 cleanup 2025-10-29 00:15:46 -07:00
SpudGunMan
17cce3b98b Update custom_scheduler.template 2025-10-28 20:23:24 -07:00
SpudGunMan
ed768b48fe Update custom_scheduler.template 2025-10-28 20:22:25 -07:00
SpudGunMan
cb8dc50424 Update install.sh 2025-10-28 20:21:29 -07:00
SpudGunMan
17cde0ca36 Update config.template 2025-10-28 20:11:20 -07:00
SpudGunMan
206b72ec4f init 2025-10-28 19:50:11 -07:00
Kelly
eadc843e27 Merge pull request #247 from SpudGunMan/copilot/enhancement-basic-scheduler
Add scheduler support for news, RSS, marine weather, system info, tide, and solar
refactored some other logic around scheduler and also the update and installer
2025-10-28 19:44:54 -07:00
SpudGunMan
14709e2828 Update scheduler.py 2025-10-28 19:43:42 -07:00
SpudGunMan
4a5d877a3d Update scheduler.py 2025-10-28 19:43:24 -07:00
SpudGunMan
0159c90708 install patch 2025-10-28 19:29:12 -07:00
SpudGunMan
05648f23f2 Update update.sh 2025-10-28 19:06:37 -07:00
SpudGunMan
f27fbdf3c9 Update scheduler.py 2025-10-28 19:01:15 -07:00
SpudGunMan
998c4078bc 🐑 2025-10-28 18:58:20 -07:00
SpudGunMan
666ae24d2c sunday 2025-10-28 18:42:29 -07:00
SpudGunMan
41e7c1207a Update scheduler.py 2025-10-28 18:00:37 -07:00
SpudGunMan
41c6de4183 Update inventory.md 2025-10-28 17:47:54 -07:00
SpudGunMan
af83ba636f not gonna
promise anything
2025-10-28 17:43:45 -07:00
copilot-swe-agent[bot]
8b54c52e7f Update config.template with new scheduler options documentation
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-29 00:41:59 +00:00
copilot-swe-agent[bot]
240dd4b46f Update documentation for new scheduler options
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-29 00:40:19 +00:00
copilot-swe-agent[bot]
7505c9ec22 Add basic scheduler support for news, readrss, mwx, sysinfo, tide, and sun
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-29 00:37:53 +00:00
SpudGunMan
14c22c8156 Create fakeNode.py 2025-10-28 17:31:56 -07:00
copilot-swe-agent[bot]
88dcce2b23 Initial plan 2025-10-29 00:31:30 +00:00
Kelly
5bc842c7e8 Merge pull request #243 from SpudGunMan/copilot/enhance-check-in-check-out
Add inventory/POS system and enhance check-in/check-out with safety monitoring
2025-10-28 17:22:42 -07:00
SpudGunMan
f73bef5894 refactor 2025-10-28 17:21:46 -07:00
SpudGunMan
9371e96feb refactor 2025-10-28 17:21:31 -07:00
SpudGunMan
85345ca45f Update db_admin.py 2025-10-28 17:21:10 -07:00
SpudGunMan
823554f689 rename template 2025-10-28 17:02:12 -07:00
SpudGunMan
5426202d51 Update system.py 2025-10-28 16:02:18 -07:00
SpudGunMan
685e0762bc Update README.md 2025-10-28 15:33:20 -07:00
SpudGunMan
8bc81cee00 docs 2025-10-28 14:02:18 -07:00
SpudGunMan
82f55c6a32 refactor
added loan items
2025-10-28 13:57:56 -07:00
SpudGunMan
be885aa00c Update inventory.md 2025-10-28 13:49:53 -07:00
SpudGunMan
536fd4deea Update checklist.py 2025-10-28 13:49:46 -07:00
SpudGunMan
eb25e55c97 Update inventory.py 2025-10-28 13:46:14 -07:00
SpudGunMan
b7f25c7c5c Update inventory.md 2025-10-28 13:43:06 -07:00
SpudGunMan
c1f1bc5eb9 docs 2025-10-28 13:40:50 -07:00
SpudGunMan
a9c00e92c7 Update checklist.py 2025-10-28 13:26:50 -07:00
SpudGunMan
713e3102f3 Update inventory.py 2025-10-28 13:22:06 -07:00
SpudGunMan
25136d1dd6 Update checklist.py 2025-10-28 13:22:02 -07:00
SpudGunMan
3795ae17ea Update mesh_bot.py 2025-10-28 13:04:23 -07:00
SpudGunMan
aef62bfbc3 archive 2025-10-28 12:55:14 -07:00
Kelly
cbb4bf0a3c Merge pull request #246 from SpudGunMan/copilot/support-js8call-integration
Add WSJT-X and JS8Call integration for forwarding digital mode messages to mesh network. Not fully tested Please test and let me know what needs changed
2025-10-28 12:49:00 -07:00
SpudGunMan
22ebc2bdbe refactor 2025-10-28 12:47:33 -07:00
SpudGunMan
517c6cbf82 Update config.template 2025-10-28 12:37:04 -07:00
copilot-swe-agent[bot]
2b0d7267b5 Optimize callsign matching performance
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 19:14:01 +00:00
copilot-swe-agent[bot]
ee4f910d6e Improve callsign matching to prevent false positives
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 19:11:58 +00:00
copilot-swe-agent[bot]
49c88306a0 Add tests and fix import issues for WSJT-X/JS8Call
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 19:08:15 +00:00
copilot-swe-agent[bot]
0f918ebccd Add documentation for WSJT-X and JS8Call integration
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 19:05:36 +00:00
copilot-swe-agent[bot]
69fac4ba98 Add WSJT-X and JS8Call integration support
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 19:02:48 +00:00
copilot-swe-agent[bot]
80745bec50 Initial plan 2025-10-28 18:54:26 +00:00
SpudGunMan
5afb1df41a Update llm.md 2025-10-28 11:52:08 -07:00
SpudGunMan
fbb7971cb0 Update llm.md 2025-10-28 11:36:46 -07:00
SpudGunMan
23c2d701df Update locationdata.py 2025-10-28 11:14:41 -07:00
SpudGunMan
2f1c305b06 tallestNode
enhancement to leaderboard thanks glocktuber
2025-10-28 11:05:48 -07:00
SpudGunMan
978fa19b56 refactor leaderboard load()
allow upgrades
2025-10-28 10:57:43 -07:00
SpudGunMan
b5de21a073 Update llm.md 2025-10-28 10:43:54 -07:00
SpudGunMan
f225c21c7a Update custom_scheduler.py 2025-10-28 06:16:32 -07:00
SpudGunMan
23ebb715c9 Update custom_scheduler.py 2025-10-28 06:13:20 -07:00
SpudGunMan
af0645f761 Update README.md 2025-10-28 06:02:53 -07:00
SpudGunMan
113750869f Update README.md 2025-10-28 05:48:34 -07:00
copilot-swe-agent[bot]
c2a18e9f9e Fix documentation clarity on penny rounding and overdue alerts
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 05:52:30 +00:00
copilot-swe-agent[bot]
fcaab86e71 Add comprehensive documentation for inventory and enhanced checklist
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 05:49:11 +00:00
copilot-swe-agent[bot]
47c84d91f1 Integrate inventory and enhanced checklist into mesh_bot
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 05:46:05 +00:00
copilot-swe-agent[bot]
8372817733 Add inventory/POS system and enhance checklist with time intervals
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-28 05:42:24 +00:00
copilot-swe-agent[bot]
9683d8b79e Initial plan 2025-10-28 05:35:13 +00:00
SpudGunMan
6f16fc6afb docs 2025-10-27 22:30:43 -07:00
SpudGunMan
fd971d8cc5 Update README.md
ffs
2025-10-27 22:23:05 -07:00
SpudGunMan
96193a22e8 LLM docs 2025-10-27 22:22:06 -07:00
SpudGunMan
02b0cde1c8 Update llm.py 2025-10-27 22:00:52 -07:00
SpudGunMan
40f4de02d9 Update system.py 2025-10-27 21:59:57 -07:00
SpudGunMan
0b1d626f09 refactor 2025-10-27 21:52:59 -07:00
SpudGunMan
964883cae9 Update system.py 2025-10-27 21:52:02 -07:00
SpudGunMan
6ab1102d07 Update wiki.py 2025-10-27 21:30:00 -07:00
SpudGunMan
c8d8880806 Update wiki.py 2025-10-27 21:25:12 -07:00
Kelly
21c2f7df18 Merge pull request #236 from SpudGunMan/copilot/link-llm-to-wiki-module
Add RAG support to LLM module with Wikipedia/Kiwix and OpenWebUI integration
2025-10-27 20:45:58 -07:00
SpudGunMan
cb51cf921b Update llm.py 2025-10-27 20:43:22 -07:00
SpudGunMan
908e84e155 Update README.md 2025-10-27 20:32:14 -07:00
SpudGunMan
b9eaf7deb0 Update wiki.py 2025-10-27 20:32:09 -07:00
SpudGunMan
128ac456eb Update wiki.py 2025-10-27 20:15:22 -07:00
SpudGunMan
1269214264 Update llm.py 2025-10-27 20:15:15 -07:00
SpudGunMan
4daf087fa5 Update llm.py 2025-10-27 20:03:14 -07:00
SpudGunMan
9282c63206 Update llm.md 2025-10-27 20:00:50 -07:00
SpudGunMan
710342447f Update llm.py 2025-10-27 19:26:53 -07:00
SpudGunMan
8e2c3a43fb refactor2 2025-10-27 18:50:58 -07:00
SpudGunMan
8d82823ccc refactor1 2025-10-27 17:31:47 -07:00
SpudGunMan
27789d7508 patch 2025-10-27 17:23:23 -07:00
SpudGunMan
680ba98a1c bumping version
thanks dependabot
2025-10-27 04:38:47 -07:00
SpudGunMan
4d71a64971 Update mesh_bot.py 2025-10-26 22:17:01 -07:00
SpudGunMan
d608754b5e dedupe 2025-10-26 21:51:02 -07:00
copilot-swe-agent[bot]
70ab741746 Update README with RAG and OpenWebUI documentation
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-27 03:35:36 +00:00
copilot-swe-agent[bot]
b0cf5914bf Add RAG support with Wikipedia/Kiwix and OpenWebUI integration
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-27 03:32:42 +00:00
copilot-swe-agent[bot]
434fbc3eef Initial plan 2025-10-27 03:26:02 +00:00
SpudGunMan
1186801d7e Update globalalert.py 2025-10-26 20:06:01 -07:00
SpudGunMan
902d764ca0 Update custom_scheduler.py 2025-10-26 19:41:48 -07:00
SpudGunMan
00fd29e679 Update custom_scheduler.py 2025-10-26 19:40:42 -07:00
SpudGunMan
163920b399 Update custom_scheduler.py 2025-10-26 19:36:44 -07:00
SpudGunMan
850ee2d291 Update wiki.py 2025-10-26 13:52:06 -07:00
SpudGunMan
cefbe93178 Update bbstools.md 2025-10-26 12:13:24 -07:00
SpudGunMan
44b2837ba0 fix typo for link command 2025-10-26 12:11:03 -07:00
SpudGunMan
1aa6a7a41a Update dxspot.py 2025-10-26 11:53:39 -07:00
SpudGunMan
7abd1fd704 Update dxspot.py
ffs
2025-10-26 11:51:31 -07:00
SpudGunMan
d35832caa8 Update dxspot.py 2025-10-26 11:49:56 -07:00
SpudGunMan
b4b0f2c561 Update dxspot.py 2025-10-26 11:40:50 -07:00
SpudGunMan
1e8ff95769 Update dxspot.py 2025-10-26 11:37:31 -07:00
SpudGunMan
41093be614 Update bbstools.md 2025-10-26 11:28:55 -07:00
SpudGunMan
6fe874e192 Update bbstools.md 2025-10-26 11:28:12 -07:00
SpudGunMan
3fa5d96073 Update bbstools.md 2025-10-26 11:25:03 -07:00
SpudGunMan
242c1c8741 Update README.md 2025-10-26 11:13:51 -07:00
SpudGunMan
c679cee66c dox 2025-10-26 11:06:48 -07:00
SpudGunMan
ca896c0f35 enhance 2025-10-26 10:19:52 -07:00
SpudGunMan
e3cd727cc3 enhance
filters
2025-10-26 10:11:22 -07:00
SpudGunMan
ded8470677 Update dxspot.py 2025-10-26 09:56:29 -07:00
SpudGunMan
f0b63b8b20 Update simulator.py 2025-10-26 09:54:25 -07:00
SpudGunMan
8a9c7a1147 changes to dx spotter
by is now of and xota is now ota
2025-10-26 09:42:02 -07:00
SpudGunMan
2d0e6b54b3 Update update.sh 2025-10-26 09:29:44 -07:00
SpudGunMan
0745847d3a Update update.sh 2025-10-26 09:28:54 -07:00
SpudGunMan
6c49c5c87f Update update.sh
https://github.com/SpudGunMan/meshing-around/issues/234
2025-10-26 09:24:14 -07:00
SpudGunMan
719fa95c1c Update README.md
thanks for this its been fun to help make this project for the community
2025-10-26 09:09:04 -07:00
SpudGunMan
b642961d26 Update README.md 2025-10-26 08:57:57 -07:00
SpudGunMan
f59d97f6ad Update mesh_bot.py 2025-10-26 08:48:29 -07:00
SpudGunMan
92d5f01ce5 allow days x for wx 2025-10-26 08:40:03 -07:00
SpudGunMan
39e53eb599 Update mesh_bot.py 2025-10-26 08:37:57 -07:00
SpudGunMan
9f6165503e Update locationdata.py 2025-10-26 08:28:41 -07:00
SpudGunMan
ec27ab65da Update custom_scheduler.py 2025-10-26 07:35:59 -07:00
SpudGunMan
f34eefb75a Update custom_scheduler.py 2025-10-26 07:34:21 -07:00
SpudGunMan
ce2ccb1455 Update scheduler.py 2025-10-26 07:31:16 -07:00
SpudGunMan
da144a2b89 scheduler enhancment
this brings scheduler into the 19th century
2025-10-26 07:26:54 -07:00
SpudGunMan
bbdccb382a Update dxspot.py 2025-10-26 06:02:07 -07:00
SpudGunMan
95f75b8e0a ... 2025-10-26 05:57:55 -07:00
SpudGunMan
0bf4915cd5 Update update.sh
https://github.com/SpudGunMan/meshing-around/issues/234
2025-10-26 05:51:59 -07:00
SpudGunMan
f83793acc9 Update README.md 2025-10-26 05:43:10 -07:00
SpudGunMan
abb2fa6b61 Update README.md 2025-10-26 05:42:51 -07:00
SpudGunMan
6d90d6f207 dx command 2025-10-26 05:37:04 -07:00
SpudGunMan
9c9e9a02e6 Update globalalert.py 2025-10-25 21:01:47 -07:00
SpudGunMan
80fc795f35 enhance 2025-10-25 18:50:12 -07:00
SpudGunMan
166c49854f Update config.template 2025-10-25 18:37:44 -07:00
SpudGunMan
a685fc3a9b Update config.template 2025-10-25 18:37:22 -07:00
SpudGunMan
91da1a4c58 Update update.sh 2025-10-25 18:34:06 -07:00
SpudGunMan
9889fd0da8 logs 2025-10-25 18:29:45 -07:00
Kelly
bebd9352ea Merge pull request #231 from SpudGunMan/copilot/add-mesh-bot-timer
Add systemd timer for daily mesh_bot_w3.service execution at 4:20 AM
2025-10-25 17:30:15 -07:00
SpudGunMan
fd1cd2a44c patch 2025-10-25 17:28:16 -07:00
SpudGunMan
ac55a51c87 # messages
https://github.com/SpudGunMan/meshing-around/issues/233
2025-10-25 16:01:54 -07:00
SpudGunMan
86144cd888 Update scheduler.py 2025-10-25 13:12:55 -07:00
SpudGunMan
d7a37ce9f1 haha 2025-10-25 12:59:59 -07:00
SpudGunMan
da7035dfed Update joke.py 2025-10-25 12:28:01 -07:00
SpudGunMan
da500981a2 Update mesh_bot.py 2025-10-25 12:23:48 -07:00
SpudGunMan
b4dc2207a6 refactor scheduler 2025-10-25 12:22:02 -07:00
SpudGunMan
b69a187466 Update mesh_bot.py 2025-10-25 12:10:57 -07:00
SpudGunMan
66d143d68e fix 2025-10-25 11:37:45 -07:00
SpudGunMan
49f2dcff88 Update test_bot.py 2025-10-25 10:59:46 -07:00
SpudGunMan
2c3c3fed10 Update INSTALL.md 2025-10-25 08:45:07 -07:00
SpudGunMan
2872fb040e refactor 2025-10-25 08:35:16 -07:00
copilot-swe-agent[bot]
6097ff899c Add explicit Unit directive to mesh_bot_w3.timer
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-25 14:02:49 +00:00
copilot-swe-agent[bot]
aec75d598a Add systemd timer to run mesh_bot_w3.service daily at 4:20 am
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-25 13:59:45 +00:00
copilot-swe-agent[bot]
0640fdbbae Initial plan 2025-10-25 13:55:40 +00:00
SpudGunMan
8312f4e683 Update system.py 2025-10-24 23:07:14 -07:00
SpudGunMan
990ea4f4e4 leaderboard fix messages 2025-10-24 21:57:16 -07:00
SpudGunMan
9acf9df3bb fix messages win 2025-10-24 21:47:32 -07:00
SpudGunMan
37942e950e fixLeaderboardLoading 2025-10-24 21:45:29 -07:00
SpudGunMan
8a0e1cba7c Update bbstools.py 2025-10-24 21:25:58 -07:00
SpudGunMan
8d0a53ec3e Update system.py 2025-10-24 21:24:18 -07:00
SpudGunMan
2ea3917eba Alllllllllll the rssssss'ssss
srsly I hope this time
2025-10-24 21:20:16 -07:00
SpudGunMan
75410c98e3 sweep 2025-10-24 20:40:23 -07:00
SpudGunMan
10171a712e moar cleanup 🧹
tighter memory control
2025-10-24 20:32:30 -07:00
SpudGunMan
fa76a76203 BIG OLD PATCH 🍠
pz days ... haha. I hope this works.
fancy potato
2025-10-24 19:54:46 -07:00
SpudGunMan
e0e275a49c Revert "scheduler memory issue"
This reverts commit bf39c2f088.
2025-10-24 18:30:08 -07:00
SpudGunMan
bf39c2f088 scheduler memory issue 2025-10-24 18:19:35 -07:00
SpudGunMan
34d36057c1 ATOM FEEDS
oh yea its 2003
2025-10-24 18:05:08 -07:00
SpudGunMan
4e1d1de883 Update adding_more.md 2025-10-24 17:52:24 -07:00
SpudGunMan
97f103dfd7 Update test_bot.py 2025-10-24 17:46:15 -07:00
SpudGunMan
47089871b1 Update test_bot.py 2025-10-24 17:45:16 -07:00
SpudGunMan
cc7ef129f6 Update test_bot.py 2025-10-24 17:42:50 -07:00
SpudGunMan
0fa5d06a3a Update test_bot.py 2025-10-24 17:12:06 -07:00
SpudGunMan
7fc44ec06e Update README.md 2025-10-24 17:12:00 -07:00
SpudGunMan
184760096e game test unit
🧩
2025-10-24 17:01:43 -07:00
SpudGunMan
8868d10388 Update hangman.py 2025-10-24 16:53:43 -07:00
SpudGunMan
1ce2ecd75c Update README.md 2025-10-24 16:50:04 -07:00
SpudGunMan
69e1c21488 enhance hangman.json
example JSON: [\"apple\",\"banana\",\"cherry\"]
2025-10-24 16:46:57 -07:00
SpudGunMan
97a2ffce7b gamepackFix
clean up globals
2025-10-24 15:46:11 -07:00
SpudGunMan
4c0d3a597e Update test_bot.py 2025-10-24 14:58:32 -07:00
SpudGunMan
094f7e61a0 Update wiki.py
fixed
2025-10-24 13:22:36 -07:00
SpudGunMan
a54ecaa5a1 Update mesh_bot.py 2025-10-24 13:21:49 -07:00
SpudGunMan
bd12392d69 Update system.py
doh
2025-10-24 13:05:40 -07:00
SpudGunMan
882bcf3f4b wiki wiki 2025-10-24 13:02:13 -07:00
SpudGunMan
c0d0ca3743 Update compose.yaml 2025-10-24 12:59:29 -07:00
SpudGunMan
d74d848646 Update compose.yaml 2025-10-24 12:58:16 -07:00
SpudGunMan
2afb915b56 Update test_bot.py 2025-10-24 12:50:58 -07:00
SpudGunMan
d5e48bead1 Update compose.yaml 2025-10-24 12:46:27 -07:00
SpudGunMan
3c80848f61 refactor wikipedia
also removed that old package!!!
2025-10-24 12:45:24 -07:00
SpudGunMan
64345fe47a Update wiki.py 2025-10-24 12:30:18 -07:00
SpudGunMan
32f734d69b Update wiki.py 2025-10-24 12:27:30 -07:00
SpudGunMan
aa6de00c5b Update wiki.py 2025-10-24 12:26:24 -07:00
SpudGunMan
6df4ba5756 Update test_bot.py
risky stuff lower
2025-10-24 12:13:50 -07:00
SpudGunMan
a11a2780db Update bbstools.py 2025-10-24 11:55:39 -07:00
SpudGunMan
980414f872 Update test_bot.py 2025-10-24 11:23:04 -07:00
SpudGunMan
f26334d625 Update wiki.py 2025-10-24 11:22:52 -07:00
SpudGunMan
24546b28d6 Create test_bot.py 2025-10-24 10:33:19 -07:00
SpudGunMan
f33da848cd cleanup 2025-10-24 10:32:28 -07:00
SpudGunMan
57ce15de4e Update radio.py 2025-10-24 10:19:05 -07:00
SpudGunMan
b8886e0662 Update qrz.py 2025-10-24 10:16:41 -07:00
SpudGunMan
9a1e86f25e Update qrz.py 2025-10-24 10:13:33 -07:00
SpudGunMan
fa8021ab5a Update checklist.py 2025-10-24 10:06:11 -07:00
SpudGunMan
f3917f1c3d Update locationdata.py 2025-10-24 10:00:35 -07:00
SpudGunMan
c1443048fd Update llm.py 2025-10-24 09:39:57 -07:00
SpudGunMan
da430557f3 Update filemon.py 2025-10-24 09:36:07 -07:00
SpudGunMan
84152bda65 Update checklist.py 2025-10-24 09:35:53 -07:00
SpudGunMan
b6e80ae576 Update bbstools.py 2025-10-24 09:18:31 -07:00
SpudGunMan
18ac26864c better resolution for gametracker
thanks pdx
2025-10-24 08:24:05 -07:00
SpudGunMan
b661fbc750 Revert "fix init of trackers"
This reverts commit 3049d18663.
2025-10-24 08:20:36 -07:00
SpudGunMan
3049d18663 fix init of trackers
thanks @pdxlocations
2025-10-24 08:12:24 -07:00
SpudGunMan
126f81fbd3 Update README.md 2025-10-23 23:38:26 -07:00
SpudGunMan
337d43a7af Update README.md 2025-10-23 23:32:53 -07:00
SpudGunMan
8c3121d5d6 Update entrypoint.sh 2025-10-23 23:29:26 -07:00
SpudGunMan
1d577c9ec5 Update compose.yaml 2025-10-23 23:12:28 -07:00
SpudGunMan
3540b8f110 Update entrypoint.sh 2025-10-23 23:10:15 -07:00
SpudGunMan
3fdebf3bf9 Update entrypoint.sh 2025-10-23 23:07:54 -07:00
SpudGunMan
430279809e Update entrypoint.sh 2025-10-23 23:06:02 -07:00
SpudGunMan
7ba3a78718 Update entrypoint.sh 2025-10-23 23:04:25 -07:00
SpudGunMan
c329391450 Update entrypoint.sh 2025-10-23 23:03:55 -07:00
SpudGunMan
6bc3c3e980 Update entrypoint.sh 2025-10-23 23:00:36 -07:00
SpudGunMan
48788ceda8 Update entrypoint.sh 2025-10-23 22:57:50 -07:00
SpudGunMan
8f5bae3b05 Update README.md 2025-10-23 22:55:18 -07:00
SpudGunMan
17c693c2f7 Update entrypoint.sh 2025-10-23 22:55:16 -07:00
SpudGunMan
ff91356c2a Update compose.yaml 2025-10-23 22:45:54 -07:00
SpudGunMan
180d9f4728 Update compose.yaml 2025-10-23 22:43:27 -07:00
SpudGunMan
1202a076d1 Update compose.yaml 2025-10-23 22:42:55 -07:00
SpudGunMan
9b62d7f4d8 Update compose.yaml 2025-10-23 22:42:00 -07:00
SpudGunMan
9451d23c09 Update compose.yaml 2025-10-23 22:40:48 -07:00
SpudGunMan
dcdef40e89 Update Dockerfile 2025-10-23 22:28:25 -07:00
SpudGunMan
817dde42f2 Update Dockerfile 2025-10-23 22:23:39 -07:00
SpudGunMan
b384d2d5b1 Update entrypoint.sh 2025-10-23 22:18:56 -07:00
SpudGunMan
4db46f16f2 Update compose.yaml 2025-10-23 22:15:12 -07:00
SpudGunMan
5590391f7e Update compose.yaml
i get no security!
2025-10-23 22:11:22 -07:00
SpudGunMan
ccb505f37f confounded 2025-10-23 22:09:34 -07:00
SpudGunMan
d883927572 enhance 2025-10-23 21:57:50 -07:00
SpudGunMan
b0109be3b0 Update README.md 2025-10-23 21:52:13 -07:00
SpudGunMan
98af757d93 enhance 2025-10-23 21:50:54 -07:00
SpudGunMan
f8746ff348 Create null 2025-10-23 21:46:59 -07:00
SpudGunMan
32fbfba3e9 config 2025-10-23 21:45:23 -07:00
SpudGunMan
d2501bf353 Update compose.yaml 2025-10-23 21:38:19 -07:00
SpudGunMan
db9d7d9790 Update README.md 2025-10-23 21:34:56 -07:00
SpudGunMan
c6b5a1c708 docker
i never enjoy docker
2025-10-23 21:26:32 -07:00
SpudGunMan
a36f1580b3 Update entrypoint.sh 2025-10-23 21:24:59 -07:00
SpudGunMan
f051e95986 Update entrypoint.sh 2025-10-23 21:24:45 -07:00
SpudGunMan
bafcfad190 Update entrypoint.sh 2025-10-23 21:21:43 -07:00
SpudGunMan
8b2059c444 Update compose.yaml 2025-10-23 21:07:43 -07:00
SpudGunMan
fd4b5607d7 Update compose.yaml 2025-10-23 21:02:42 -07:00
SpudGunMan
df30ee9cc4 Update compose.yaml 2025-10-23 20:53:26 -07:00
SpudGunMan
c1135ecadf cleanup 2025-10-23 20:40:00 -07:00
SpudGunMan
899702eecc Update compose.yaml 2025-10-23 20:26:55 -07:00
SpudGunMan
d4604d8cbd Update compose.yaml 2025-10-23 20:23:03 -07:00
SpudGunMan
c674b0a404 Update scheduler.py 2025-10-23 20:18:42 -07:00
SpudGunMan
d59ddfd517 fix for Malice
sorry this was so painfull
2025-10-23 20:15:15 -07:00
SpudGunMan
f68c533488 errorLoggin 2025-10-23 20:08:42 -07:00
SpudGunMan
6e47d71028 Update Dockerfile 2025-10-23 20:00:02 -07:00
SpudGunMan
f9af9b756d Update compose.yaml
how did I miss this
2025-10-23 19:57:18 -07:00
SpudGunMan
c19d442190 Update compose.yaml 2025-10-23 19:53:01 -07:00
SpudGunMan
4c2d0cdebb Update compose.yaml 2025-10-23 19:49:01 -07:00
SpudGunMan
612dbf01d3 Update compose.yaml 2025-10-23 19:47:40 -07:00
SpudGunMan
28846b24a6 Update compose.yaml 2025-10-23 19:40:43 -07:00
SpudGunMan
cd398375a2 Update compose.yaml 2025-10-23 19:33:40 -07:00
SpudGunMan
01372a0f2c Update compose.yaml 2025-10-23 19:30:08 -07:00
SpudGunMan
8254ec5baf Update compose.yaml 2025-10-23 19:23:25 -07:00
SpudGunMan
549e12ffc1 Update Dockerfile 2025-10-23 18:50:47 -07:00
SpudGunMan
d940cdf534 Update Dockerfile 2025-10-23 15:57:34 -07:00
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
SpudGunMan
8c1cbaf442 fix game bugs 2025-10-12 13:05:54 -07:00
SpudGunMan
8d6a95b5da OMG
sorry to the fans
2025-10-12 11:00:09 -07:00
SpudGunMan
b4b2ef3d80 Update system.py
expand the timers for game play to 3 days for cleanup
2025-10-12 10:05:30 -07:00
SpudGunMan
13b1b90864 Update system.py 2025-10-12 10:00:09 -07:00
SpudGunMan
838bd3edce Update radio.py
oops
2025-10-12 09:55:55 -07:00
SpudGunMan
70bcf43b49 Update radio.py 2025-10-12 09:54:56 -07:00
SpudGunMan
0a02ae860e Update radio.py 2025-10-11 22:21:09 -07:00
SpudGunMan
a80575a381 Update radio.py 2025-10-11 22:20:52 -07:00
SpudGunMan
e57b65b447 Update system.py 2025-10-11 21:58:39 -07:00
SpudGunMan
1d18f0936c Update radio.py 2025-10-11 21:25:52 -07:00
SpudGunMan
b096716b96 enhance import note 2025-10-11 21:19:26 -07:00
SpudGunMan
f4734c5b87 vox detection 2025-10-11 20:44:03 -07:00
SpudGunMan
96447b166f Update README.md 2025-10-11 14:04:41 -07:00
SpudGunMan
2fc151bbbf leaderboard
I hope its all working now!
2025-10-11 12:55:56 -07:00
SpudGunMan
e66af5c068 bug in leaderboard fix 2025-10-11 12:47:24 -07:00
SpudGunMan
d3ce4d3905 Update addFav.py 2025-10-11 12:38:20 -07:00
SpudGunMan
a95cdeb086 -pickle
or print either way
2025-10-11 12:35:58 -07:00
SpudGunMan
27bf61a913 Update addFav.py 2025-10-11 12:21:00 -07:00
SpudGunMan
d62990b6db Update system.py
seriously open a window
2025-10-11 12:10:03 -07:00
SpudGunMan
0784aaebd9 enhance 2025-10-11 12:09:18 -07:00
SpudGunMan
e348854a50 cleanup 2025-10-11 11:46:09 -07:00
SpudGunMan
a71e5fa8f3 Update addFav.py 2025-10-11 11:22:21 -07:00
SpudGunMan
9600ea5e00 enhance with client_base 2025-10-11 11:20:00 -07:00
SpudGunMan
cc7461929e cleanup 2025-10-11 11:19:52 -07:00
SpudGunMan
0c8fb0c243 cleanup 2025-10-11 07:54:20 -07:00
SpudGunMan
311563320e enhance
rss sucks
2025-10-10 19:41:44 -07:00
SpudGunMan
78b6d660dd yakima works 2025-10-10 16:37:30 -07:00
SpudGunMan
77da966b9d Update mesh_bot.py 2025-10-10 16:22:59 -07:00
SpudGunMan
d844c123be refactor Rivers 2025-10-10 16:02:23 -07:00
SpudGunMan
d4d36c8a31 increase urlTimeout to 15 seconds 2025-10-10 15:30:36 -07:00
SpudGunMan
9acd57a420 log in the river
better logs for this API
2025-10-10 15:21:36 -07:00
SpudGunMan
bc06712b87 enhance rssread
enhance rssread
enhance rssread
enhance rssread
2025-10-10 12:50:07 -07:00
SpudGunMan
260e52fe81 Update system.py 2025-10-10 11:53:33 -07:00
SpudGunMan
6b548f82b2 Update mesh_bot.py 2025-10-10 11:40:30 -07:00
SpudGunMan
2273b481ad NEW CHUNKER
what day is it, chunker day!
2025-10-10 11:38:01 -07:00
SpudGunMan
95ee7779b4 Update mesh_bot.py
@mesb1 ahhh thanks!
2025-10-10 09:47:32 -07:00
SpudGunMan
ee1391f6e7 2fa.ini 2025-10-10 07:41:18 -07:00
SpudGunMan
b7a0d7cd8e Update system.py 2025-10-10 07:30:18 -07:00
SpudGunMan
a880236117 Update filemon.py 2025-10-10 07:24:42 -07:00
SpudGunMan
a67bdc3641 Update filemon.py 2025-10-10 07:16:02 -07:00
SpudGunMan
da8235adae Update filemon.py 2025-10-10 07:08:51 -07:00
SpudGunMan
22384463e2 are you human
or are you dancer, this was just fun to add. 2fa human check to x: commands
2025-10-10 07:03:10 -07:00
SpudGunMan
b48377de5f Update system.py 2025-10-10 00:42:31 -07:00
SpudGunMan
855c2e08cc Update system.py 2025-10-10 00:41:30 -07:00
SpudGunMan
d0aa07ed7d Update system.py
i should snooze
2025-10-10 00:39:43 -07:00
SpudGunMan
7328a92535 Update system.py 2025-10-10 00:36:34 -07:00
SpudGunMan
35c8dc6f70 resetLeaderboard 2025-10-10 00:36:22 -07:00
SpudGunMan
4b1123dcac Update mesh_bot.py 2025-10-10 00:06:19 -07:00
SpudGunMan
b74dc1ff25 not this or that 2025-10-09 23:31:21 -07:00
SpudGunMan
8c752dff3e Update system.py 2025-10-09 23:11:08 -07:00
SpudGunMan
b3b45a4335 Update system.py 2025-10-09 19:10:58 -07:00
SpudGunMan
fd86187798 Update mesh_bot.py 2025-10-09 19:09:20 -07:00
SpudGunMan
4da3e68c62 readrss
thanks FJRP you can now return an rss feed
2025-10-09 19:06:38 -07:00
SpudGunMan
e47907ebeb Update system.py 2025-10-09 18:38:35 -07:00
SpudGunMan
f63278ae8f Update system.py 2025-10-09 18:37:04 -07:00
SpudGunMan
0e0d2f11d7 Update system.py 2025-10-09 18:11:40 -07:00
SpudGunMan
6587ba61e2 Update system.py 2025-10-09 18:10:02 -07:00
SpudGunMan
b46697c0c4 enhance leaderboard 2025-10-09 18:08:38 -07:00
SpudGunMan
f5f8539924 segassem
reverse the order of the messages
2025-10-09 17:21:07 -07:00
SpudGunMan
e8063fcf3f Update system.py 2025-10-09 17:05:01 -07:00
SpudGunMan
169f9b27a5 Update system.py 2025-10-09 17:03:39 -07:00
SpudGunMan
4ceb23bcff Update system.py 2025-10-09 17:02:14 -07:00
SpudGunMan
315ae84bb6 enhance 2025-10-09 16:27:06 -07:00
SpudGunMan
fb12c11a7e Update mesh_bot.py 2025-10-09 15:39:32 -07:00
SpudGunMan
496c222cdc Update README.md 2025-10-09 15:34:56 -07:00
Kelly
cb55aba498 Merge pull request #203 from SpudGunMan/copilot/add-local-wiki-search-functionality
Add Kiwix local wiki server support for offline Wikipedia searches
2025-10-08 20:38:17 -07:00
SpudGunMan
1aac3d5ac2 failover 2025-10-08 20:37:21 -07:00
SpudGunMan
413f2a24d9 Kiwix in wiki
@NomDeTom its finally done
2025-10-08 20:29:52 -07:00
SpudGunMan
c75782d559 Update system.py 2025-10-08 20:03:05 -07:00
SpudGunMan
d38314a21c Update system.py 2025-10-08 19:31:30 -07:00
SpudGunMan
65dfe90edc Update system.py 2025-10-08 19:24:14 -07:00
SpudGunMan
3ce24fb7c9 Update system.py 2025-10-08 12:48:26 -07:00
SpudGunMan
8765e5a871 🥒
ahh pickles
2025-10-08 12:45:37 -07:00
SpudGunMan
b4f0421423 Update system.py 2025-10-08 12:37:52 -07:00
SpudGunMan
2b8906ae55 colon
KFC🍗
2025-10-08 11:11:14 -07:00
Kelly
5710b47a99 Merge pull request #208 from SpudGunMan/copilot/add-funny-data-facts-logging
Add mesh leaderboard feature to track extreme metrics and special packets
2025-10-08 11:02:42 -07:00
SpudGunMan
1f8bf5a700 bytes&bits 2025-10-08 11:01:21 -07:00
copilot-swe-agent[bot]
171480b704 Add leaderboard command documentation to README
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 16:53:07 +00:00
copilot-swe-agent[bot]
c74e4f99b2 Add mesh leaderboard feature to track extreme metrics
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 16:50:46 +00:00
copilot-swe-agent[bot]
cd5749521c Initial plan 2025-10-08 16:43:39 +00:00
Kelly
5ce7019dba Merge pull request #204 from SpudGunMan/copilot/fix-messages-bot-errors
Fix messages command error with Unicode characters (Cyrillic) I like this approach thanks GitHub
2025-10-08 09:21:22 -07:00
SpudGunMan
6c1e0cc2f9 bits&bytes 2025-10-08 09:20:09 -07:00
SpudGunMan
68b171f68e Update mesh_bot.py 2025-10-08 08:42:44 -07:00
SpudGunMan
7cfd5d0b0e Update mesh_bot.py 2025-10-08 08:39:24 -07:00
SpudGunMan
6dd4f0c4b6 Update joke.py 2025-10-08 08:37:03 -07:00
copilot-swe-agent[bot]
8ef0fa2ac0 Fix messages command to handle Unicode characters safely
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:18:30 +00:00
copilot-swe-agent[bot]
0c8d6b8fac Initial plan 2025-10-08 15:12:01 +00:00
copilot-swe-agent[bot]
1e4e5e6627 Add documentation and fix deprecated BeautifulSoup method
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:05:09 +00:00
copilot-swe-agent[bot]
c97004b410 Add Kiwix local wiki server support with configuration options
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-08 15:00:44 +00:00
copilot-swe-agent[bot]
2292fb2655 Initial plan 2025-10-08 14:54:32 +00:00
SpudGunMan
3ebf3ba374 Update config.template 2025-10-08 07:05:40 -07:00
SpudGunMan
b6087c926c news.template 2025-10-08 01:22:08 -07:00
SpudGunMan
2895e6c034 newNews🚨🚨
the location of news.txt changed FYI 🚨 now you can read more files
2025-10-08 01:14:10 -07:00
SpudGunMan
691bc8d701 Update README.md 2025-10-08 00:02:08 -07:00
SpudGunMan
bd50524e95 Update README.md 2025-10-07 23:53:22 -07:00
SpudGunMan
299b749f0e Update survey.py
I should sleep
2025-10-07 23:44:52 -07:00
SpudGunMan
9a060e3c6e Update quiz.py 2025-10-07 23:43:34 -07:00
SpudGunMan
a012ef17d0 Update survey.py 2025-10-07 23:41:22 -07:00
SpudGunMan
adbf78b740 enhance 2025-10-07 23:39:23 -07:00
SpudGunMan
3aad8d89cf Update survey.py 2025-10-07 23:34:36 -07:00
SpudGunMan
3370304249 Update survey.py 2025-10-07 23:30:35 -07:00
SpudGunMan
ef62a06db1 Update settings.py
disabled till configured and also uses io
2025-10-07 23:16:28 -07:00
SpudGunMan
8cc1d24b93 Update README.md 2025-10-07 23:15:23 -07:00
SpudGunMan
fca90cbee3 Update survey.py 2025-10-07 23:10:09 -07:00
SpudGunMan
d05c7bb6a5 Update survey.py 2025-10-07 23:05:12 -07:00
SpudGunMan
7774529fb4 bugfix 2025-10-07 23:02:52 -07:00
SpudGunMan
4c615af22d Update mesh_bot.py 2025-10-07 22:42:13 -07:00
SpudGunMan
6c078b4d17 Survey Says!
is this cool?
2025-10-07 22:39:08 -07:00
SpudGunMan
ddb9c8b4bf Update mesh_bot.py 2025-10-07 20:34:17 -07:00
SpudGunMan
73f3175705 Update mesh_bot.py 2025-10-07 20:18:12 -07:00
SpudGunMan
d2ee1bce1c Update quiz.py 2025-10-07 20:01:20 -07:00
SpudGunMan
b4a2149815 enhance 2025-10-07 20:00:22 -07:00
SpudGunMan
320f41e05a documentation 2025-10-07 17:58:19 -07:00
SpudGunMan
48a57e875f QuizMaster
let me know if this is cool
2025-10-07 17:48:22 -07:00
SpudGunMan
ce317d8bbe Update simulator.py 2025-10-07 16:35:03 -07:00
SpudGunMan
c2d2a8f7e4 Update simulator.py 2025-10-07 16:33:33 -07:00
SpudGunMan
00280e351c Update mesh_bot.py 2025-10-07 14:04:07 -07:00
SpudGunMan
0e8bb197a9 Update mesh_bot.py 2025-10-07 13:59:49 -07:00
SpudGunMan
d825c0fa15 Update mesh_bot.py
what happened here? I forget now but sheesh!
2025-10-07 13:57:00 -07:00
SpudGunMan
6abe73c1bc Update mesh_bot.py
ack
2025-10-07 13:54:32 -07:00
SpudGunMan
b8e9adb223 fixMessagesCommand
thanks @mesb1  https://github.com/SpudGunMan/meshing-around/issues/200
2025-10-07 13:48:23 -07:00
SpudGunMan
e621016e9a nom
nom
2025-10-07 06:06:21 -07:00
SpudGunMan
cfaf652852 Update mesh_bot.py 2025-10-06 20:02:36 -07:00
SpudGunMan
6c27b5d5de xoxo
enhance
2025-10-06 18:03:22 -07:00
SpudGunMan
a31fa90942 Update system.py 2025-10-06 14:57:40 -07:00
SpudGunMan
3cd347dff3 Update tictactoe.py 2025-10-06 14:46:24 -07:00
SpudGunMan
ea4ac1f9c1 whichonelooksbetter 2025-10-06 14:42:50 -07:00
SpudGunMan
a9da8336cc enhance 2025-10-06 14:40:08 -07:00
SpudGunMan
4ba60ed276 correctLogLevel 2025-10-06 14:25:13 -07:00
Kelly
42e07d44e6 Merge pull request #198 from martinbogo/feature/tictactoe-game
Feature/tictactoe game
2025-10-06 14:11:23 -07:00
SpudGunMan
11f5218c2e Merge branch 'feature/tictactoe-game' of https://github.com/martinbogo/meshing-around into pr/198 2025-10-06 14:08:49 -07:00
SpudGunMan
e137420138 patch-2 2025-10-06 14:08:07 -07:00
Kelly
80c0f698b6 Update modules/games/tictactoe.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 13:51:56 -07:00
SpudGunMan
2045bf98f7 🧩 2025-10-06 13:45:02 -07:00
SpudGunMan
c36ce2c3a6 Update mesh_bot.py 2025-10-06 13:09:47 -07:00
SpudGunMan
7ff36a3d5f Update mesh_bot.py 2025-10-06 13:05:32 -07:00
SpudGunMan
ae1a3040b5 patches
dont need no stinking patches. thanks again.
2025-10-06 12:54:41 -07:00
Martin Bogomolni
84b6b48d60 feat: Add tic-tac-toe game with compact messaging
🎮 New Tic-Tac-Toe Game Features:
- Compact 3x3 board display using ASCII art
- Smart AI opponent with win/block/random strategy
- All messages under 200-character meshtastic limit (tested: 10-50 chars)
- Player vs Bot gameplay with X (player) vs O (bot)
- Win detection for rows, columns, and diagonals
- Tie game detection when board is full
- Game statistics tracking (games played, won)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

and

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

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

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

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

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

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

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

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

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

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

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

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

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

also added the awareness of some extra bits
and added a new tracker for all nodes seenNodes
2024-12-09 12:37:31 -08:00
SpudGunMan
a53f5a033b Update mesh_bot.py 2024-12-09 00:27:44 -08:00
SpudGunMan
ea37405149 Update mesh_bot.py 2024-12-08 19:48:54 -08:00
SpudGunMan
e16ecbe1b7 Update README.md 2024-12-08 19:44:51 -08:00
SpudGunMan
db6f20dd3b SMTPConfig 2024-12-08 19:38:58 -08:00
SpudGunMan
9fa60d0c84 Update config.template 2024-12-08 19:27:10 -08:00
SpudGunMan
2fdad79dbb SMTP module work 2024-12-08 19:23:54 -08:00
SpudGunMan
20342fb58c Update smtp.py 2024-12-08 14:39:50 -08:00
SpudGunMan
b7e815cf85 Update smtp.py 2024-12-08 14:36:55 -08:00
SpudGunMan
8e3d1c432e Update smtp.py 2024-12-08 14:34:53 -08:00
SpudGunMan
1a8ed573a8 Update smtp.py 2024-12-08 14:34:04 -08:00
SpudGunMan
63516b36e4 sentMultipleSMS 2024-12-08 14:33:01 -08:00
SpudGunMan
d17b05a40a Update smtp.py 2024-12-08 14:29:26 -08:00
SpudGunMan
e4cefa2264 Update smtp.py 2024-12-08 14:28:22 -08:00
SpudGunMan
90bf3459c9 typeSetCommands 2024-12-08 14:21:51 -08:00
SpudGunMan
0983259117 banList 2024-12-08 14:16:45 -08:00
SpudGunMan
377e5a9825 multipleSMS
clearSMS
2024-12-08 14:11:43 -08:00
SpudGunMan
7edcb4457a Update smtp.py 2024-12-08 13:51:23 -08:00
SpudGunMan
3fec7867d9 Update filemon.py 2024-12-08 13:28:23 -08:00
SpudGunMan
7e447616d9 IMAP
all untested still
2024-12-08 13:06:56 -08:00
SpudGunMan
e59c3de0aa Update smtp.py 2024-12-08 12:54:09 -08:00
SpudGunMan
db808568cb Update settings.py 2024-12-08 11:32:54 -08:00
SpudGunMan
0615733445 enhance emergencyResponder 2024-12-08 11:32:30 -08:00
SpudGunMan
402c58c111 emergencyResponder 2024-12-08 11:27:34 -08:00
SpudGunMan
dde6c2ed32 Update README.md 2024-12-08 09:46:45 -08:00
SpudGunMan
766ff0a195 SMTP Module
inital idea
2024-12-07 21:18:46 -08:00
SpudGunMan
d614cbcff5 Update README.md 2024-12-07 16:06:05 -08:00
SpudGunMan
81798c1fc2 Update locationdata.py 2024-12-07 15:11:05 -08:00
SpudGunMan
210a75671f FEMAipaws
not functioning yet
2024-12-07 14:50:25 -08:00
SpudGunMan
f3e113dcc1 Update README.md 2024-12-07 00:27:24 -08:00
SpudGunMan
145664a42f Update llm.py 2024-12-07 00:04:01 -08:00
SpudGunMan
acc770732e Update config.template 2024-12-07 00:03:52 -08:00
Kelly
ded4c79911 Merge pull request #87 from propstg/game-fixes 2024-12-06 23:26:59 -08:00
propstg
ad0c9c710f Add error message when trying to buy max when inventory full, instead of sending usage message 2024-12-07 01:54:47 -05:00
propstg
259c4991f9 Show name property instead of object's tostring 2024-12-07 01:20:00 -05:00
SpudGunMan
5fe185ab7f Update README.md 2024-12-06 13:42:16 -08:00
SpudGunMan
974caaff42 Update system.py 2024-12-06 13:18:52 -08:00
SpudGunMan
41d8758969 enhanceChunkr
better logic?
2024-12-06 12:59:46 -08:00
SpudGunMan
92e1e3168e Update pong_bot.py 2024-12-06 11:35:06 -08:00
SpudGunMan
a608e29911 Update mesh_bot.py 2024-12-06 11:34:57 -08:00
SpudGunMan
015b72c8c6 Update requirements.txt 2024-12-06 11:14:56 -08:00
SpudGunMan
74cf5841ff Update README.md 2024-12-06 11:14:40 -08:00
SpudGunMan
9ba7b1c972 remove Ollama Requirement
remove and replace with API call to web only
2024-12-06 10:09:07 -08:00
SpudGunMan
5bf0417203 Update README.md 2024-12-05 23:23:50 -08:00
SpudGunMan
2b7a20f8d9 Update pong_bot.py 2024-12-05 23:19:55 -08:00
SpudGunMan
2afb49cbc7 Update README.md 2024-12-05 23:16:18 -08:00
SpudGunMan
17008b7711 Update README.md 2024-12-05 23:14:36 -08:00
SpudGunMan
36ff328380 Update eas_alert_parser.py 2024-12-05 17:18:48 -08:00
SpudGunMan
bb051f4225 errata 2024-12-05 12:49:29 -08:00
SpudGunMan
61c5be1a08 throttleEAS 2024-12-05 11:39:36 -08:00
SpudGunMan
bc7d47b2a7 Update README.md 2024-12-05 11:32:46 -08:00
SpudGunMan
24bcd5cbf9 readNews
on demand return of the file news.txt for readnews onair
2024-12-05 11:08:11 -08:00
SpudGunMan
8407512b0f fineTuneEAS 2024-12-04 23:31:29 -08:00
SpudGunMan
6f4e8615a3 Update system.py 2024-12-04 20:06:33 -08:00
SpudGunMan
314d36e0dc fixReporter 2024-12-04 15:28:18 -08:00
SpudGunMan
27accb0d4a Update bbstools.py 2024-12-02 16:33:30 -08:00
SpudGunMan
fd84505ad1 typo 2024-12-02 16:25:48 -08:00
SpudGunMan
8f75b13c4d Update mesh_bot.py 2024-12-02 16:23:35 -08:00
SpudGunMan
31d05f8aa7 Update mesh_bot.py 2024-12-02 16:21:40 -08:00
SpudGunMan
cdfe4bb844 Update bbstools.py 2024-12-02 16:14:40 -08:00
SpudGunMan
f30e9cd8b8 Update bbstools.py 2024-12-02 16:14:01 -08:00
SpudGunMan
931bc7b9f7 Update bbstools.py 2024-12-02 16:08:22 -08:00
SpudGunMan
049c0d5ad7 bbsLinkEnhancments 2024-12-02 16:05:14 -08:00
SpudGunMan
a5f1e452e4 Update bbstools.py 2024-12-01 13:13:20 -08:00
SpudGunMan
d89cd8598d limitAutoPing 2024-11-29 20:18:52 -08:00
SpudGunMan
d4e3ea60e3 Update settings.py 2024-11-29 18:23:22 -08:00
SpudGunMan
b98bc8429a Update mesh_bot.py 2024-11-29 18:17:17 -08:00
SpudGunMan
4bb7c9296a Update README.md 2024-11-29 18:06:42 -08:00
SpudGunMan
bb7b5b1c90 scheduler work 2024-11-29 18:05:07 -08:00
SpudGunMan
c400f6f998 Update README.md 2024-11-29 17:27:00 -08:00
SpudGunMan
fce6c0b2e4 Update mesh_bot.py 2024-11-29 17:13:30 -08:00
SpudGunMan
0d0288ba18 Update mesh_bot.py 2024-11-29 17:11:26 -08:00
SpudGunMan
c25d7bc8de Update mesh_bot.py 2024-11-29 17:07:00 -08:00
SpudGunMan
d42fa72d54 fix bbslink/ack 2024-11-29 17:05:28 -08:00
SpudGunMan
bc7176c1cf Update README.md 2024-11-29 11:23:57 -08:00
SpudGunMan
15d454f93a Update eas_alert_parser.py 2024-11-29 10:37:52 -08:00
SpudGunMan
249ee3bb5a Update README.md 2024-11-29 00:27:38 -08:00
SpudGunMan
a3b3d4ea0e Update mesh_bot.py 2024-11-28 23:28:42 -08:00
SpudGunMan
27f9d04538 Update system.py 2024-11-28 23:02:16 -08:00
SpudGunMan
03f1869b23 Update mesh_bot.py 2024-11-28 22:59:49 -08:00
SpudGunMan
479e177a64 exceed the maxBuffer fix 2024-11-28 22:24:41 -08:00
SpudGunMan
5cf166af87 Update mesh_bot.py 2024-11-28 22:06:07 -08:00
SpudGunMan
e24bcd7d38 Update mesh_bot.py 2024-11-28 21:43:03 -08:00
SpudGunMan
768898df64 Update system.py 2024-11-28 21:38:37 -08:00
SpudGunMan
cf282e04bb Update system.py 2024-11-28 21:16:09 -08:00
SpudGunMan
db4edac083 enhance maBuffer Logic 2024-11-28 21:14:41 -08:00
SpudGunMan
877d0cf7f8 enhance MaxBuffer Test
sets the lower limit to 150
2024-11-28 19:35:18 -08:00
SpudGunMan
e78c441a6e Update README.md 2024-11-28 17:23:09 -08:00
SpudGunMan
e945819365 Update mesh_bot.py 2024-11-28 17:12:29 -08:00
SpudGunMan
23e8db50fd Update README.md 2024-11-28 17:11:04 -08:00
SpudGunMan
193ffe6394 Update system.py 2024-11-28 16:53:55 -08:00
SpudGunMan
87016186d8 Update system.py 2024-11-28 16:53:04 -08:00
SpudGunMan
d7d96a89cf Update system.py 2024-11-28 16:51:58 -08:00
SpudGunMan
aa5ef23363 maxBuffer
test 4 will divide the maxBuffer value and send junk data to test a radio or network
2024-11-28 16:41:49 -08:00
SpudGunMan
c18e0401e4 auto TEST Buffer 2024-11-28 16:17:57 -08:00
SpudGunMan
8568990295 Update system.py 2024-11-28 15:55:44 -08:00
SpudGunMan
44e6460224 Update mesh_bot.py 2024-11-28 15:28:58 -08:00
SpudGunMan
d53480290c wantAck 2024-11-28 14:56:49 -08:00
SpudGunMan
1499d883bc dadJokes🥔🚫 2024-11-28 13:20:30 -08:00
SpudGunMan
883a6902fa Update locationdata.py 2024-11-28 13:06:28 -08:00
SpudGunMan
6d3b754c6c bugFix 2024-11-28 00:02:26 -08:00
SpudGunMan
62f73ce2e6 Update README.md 2024-11-27 23:17:05 -08:00
Kelly
eeab9f3fb1 Merge pull request #86 from SpudGunMan/lab
Enhancements 🦃
2024-11-27 23:01:45 -08:00
SpudGunMan
c21a67d1cf Update README.md 2024-11-27 22:58:47 -08:00
SpudGunMan
afe48a44da fixEAS Multi Channel 2024-11-27 22:50:30 -08:00
SpudGunMan
7e4822e4ec Update locationdata.py 2024-11-27 22:22:19 -08:00
SpudGunMan
705ab6a980 fixClosesNodex2 2024-11-27 21:31:52 -08:00
SpudGunMan
963b29eea4 fixEnhanceAutoPing 2024-11-27 21:20:08 -08:00
SpudGunMan
b3f889c4c7 fixBugClosestNodes 2024-11-27 20:40:08 -08:00
SpudGunMan
545b4891b4 error2Warning 2024-11-27 20:38:38 -08:00
SpudGunMan
c89f14b3c2 fix that needed for later 2024-11-25 11:51:25 -08:00
SpudGunMan
c416b00383 newLogRotation 2024-11-25 11:49:39 -08:00
SpudGunMan
669a891eeb Update log.py 2024-11-25 11:46:22 -08:00
SpudGunMan
520d58b262 Update log.py 2024-11-25 11:35:36 -08:00
SpudGunMan
24dff868ff RotateLogger
default is 32 days of logs configure if needed otherwise.
2024-11-24 19:52:25 -08:00
SpudGunMan
cf45bb5060 LOG Rotation Update
update log handler
2024-11-24 19:43:47 -08:00
SpudGunMan
0f9064f2c3 EAS API Alerts
Enable EAS API Messages to Mesh
Fix multiping Device 2
2024-11-23 16:27:55 -08:00
SpudGunMan
f94f329b1f Create eas_alert_parser.py 2024-11-23 15:50:04 -08:00
SpudGunMan
dc4560081d dropLangChain 2024-11-23 15:49:27 -08:00
SpudGunMan
b42cd0e6dc Update README.md 2024-11-20 16:06:28 -08:00
SpudGunMan
bbe1e45541 Update README.md 2024-11-20 16:03:22 -08:00
SpudGunMan
2c61db1215 fix that enhance 2024-11-19 19:52:04 -08:00
SpudGunMan
fde2bb94d9 enhance 2024-11-19 19:51:40 -08:00
SpudGunMan
436a43d3ad Update README.md 2024-11-19 19:49:46 -08:00
SpudGunMan
6b2a6f3a83 enhanceFileMon 2024-11-19 19:44:49 -08:00
SpudGunMan
8e5773115c FileMon
Enhancement with FileMon to watch a file and deliver its goods to the mesh
2024-11-19 19:41:14 -08:00
SpudGunMan
626a5dfe16 moveAPItide 2024-11-15 16:21:56 -08:00
SpudGunMan
e63f4816c4 Update locationdata.py 2024-11-15 14:16:35 -08:00
SpudGunMan
13852b194b enhance 2024-11-11 15:11:25 -08:00
SpudGunMan
a68c20098b ScrubUno
gone but not forgotten
2024-11-11 14:41:14 -08:00
Kelly
432b5a767e Merge pull request #85 from SpudGunMan/lab
BBS LiNK
2024-11-07 15:01:54 -08:00
SpudGunMan
952659198c fixes 2024-11-05 07:45:19 -08:00
SpudGunMan
4e518758e5 Update bbstools.py 2024-10-31 16:56:43 -07:00
SpudGunMan
e1b3dd311f Update bbstools.py 2024-10-31 16:42:09 -07:00
SpudGunMan
bb0f923155 bbslink
first attempt at giving BBS link over the air
2024-10-31 16:38:16 -07:00
SpudGunMan
ab86f02bd7 Update llm.py 2024-10-27 17:12:28 -07:00
SpudGunMan
43067cfb07 Update llm.py 2024-10-27 16:58:05 -07:00
SpudGunMan
3300694059 Update mesh_bot.py 2024-10-26 18:54:48 -07:00
SpudGunMan
f59b8715dd Update simulator.py 2024-10-26 17:31:05 -07:00
SpudGunMan
60abadd1fc Update llm.py 2024-10-24 19:49:01 -07:00
SpudGunMan
4297c91c5e Update llm.py 2024-10-24 19:47:19 -07:00
SpudGunMan
c8eddc3787 Update llm.py 2024-10-24 19:44:51 -07:00
SpudGunMan
d01d81a6d7 openWebUI 2024-10-24 19:21:23 -07:00
SpudGunMan
40b31fd8af Update dopewar.py 2024-10-23 22:13:48 -07:00
SpudGunMan
7b995b35cd 💊
cleanup display on some things
2024-10-23 22:07:46 -07:00
SpudGunMan
00885d57c9 Update dopewar.py 2024-10-23 21:21:44 -07:00
SpudGunMan
d03d7dbc47 Update llm.py 2024-10-21 19:36:02 -07:00
SpudGunMan
7fd4074bd3 Update videopoker.py 2024-10-20 22:56:58 -07:00
SpudGunMan
8367bca4d5 Update joke.py 2024-10-20 17:42:00 -07:00
SpudGunMan
5059990adb Update mesh_bot.py 2024-10-20 17:24:22 -07:00
SpudGunMan
9dd9d39df4 Update llm.py 2024-10-19 08:14:23 -07:00
SpudGunMan
87f89fea6d Update llm.py 2024-10-18 10:50:29 -07:00
Kelly
8433b3cf5f Merge pull request #84 from SpudGunMan/lab
LotsaBugs
2024-10-18 10:30:57 -07:00
SpudGunMan
eb9f1eb4c2 Update requirements.txt 2024-10-18 10:29:08 -07:00
SpudGunMan
7308597f23 Update requirements.txt 2024-10-18 10:28:08 -07:00
SpudGunMan
a4837cf337 mainline 2024-10-18 10:26:52 -07:00
SpudGunMan
dd8ba14bbe Update mesh_bot.py 2024-10-18 10:21:28 -07:00
SpudGunMan
5a5b394a17 Update mesh_bot.py 2024-10-16 13:27:59 -07:00
SpudGunMan
ab8eb41853 Update golfsim.py 2024-10-15 20:11:35 -07:00
SpudGunMan
38bfcc1581 Update llm.py 2024-10-15 19:33:04 -07:00
SpudGunMan
1c7840a203 Update llm.py 2024-10-15 19:32:29 -07:00
SpudGunMan
b5fb7e997c Update README.md 2024-10-15 19:31:17 -07:00
SpudGunMan
1df2fd3486 channelFix 2024-10-15 13:25:53 -07:00
SpudGunMan
957619933d Update report_generator5.py 2024-10-14 21:38:40 -07:00
SpudGunMan
3f9fdb10a3 enhance 2024-10-14 21:30:54 -07:00
SpudGunMan
bf48a61766 Update llm.py 2024-10-14 18:00:20 -07:00
SpudGunMan
6a1606ca6c Update llm.py 2024-10-14 17:48:10 -07:00
SpudGunMan
7551ff2ecb Update llm.py 2024-10-14 13:07:00 -07:00
SpudGunMan
20e864b672 oops 2024-10-14 12:41:54 -07:00
SpudGunMan
91f7ea072f ragConcept 2024-10-14 12:39:37 -07:00
SpudGunMan
8b62d70562 Update golfsim.py 2024-10-13 16:06:10 -07:00
SpudGunMan
fd43eb0ea1 enhance
single interface no public channel bug fix
2024-10-12 19:18:46 -07:00
SpudGunMan
759b89f790 Update system.py 2024-10-12 18:36:08 -07:00
SpudGunMan
54a27df86d finesse 2024-10-12 18:33:01 -07:00
SpudGunMan
4a99c44702 Update simulator.py 2024-10-12 18:03:24 -07:00
SpudGunMan
ed01b1fb87 Update mesh_bot.py 2024-10-12 16:48:19 -07:00
SpudGunMan
f3e135e5f8 Update mesh_bot.py 2024-10-12 16:47:56 -07:00
SpudGunMan
c51ef99946 cant believe this got forgot 2024-10-12 16:22:52 -07:00
SpudGunMan
01ee3f7418 Update golfsim.py 2024-10-12 16:12:59 -07:00
SpudGunMan
88e58e65ed Update lemonade.py 2024-10-12 16:12:46 -07:00
SpudGunMan
83673981f2 Update mesh_bot.py 2024-10-12 15:55:55 -07:00
SpudGunMan
dd398ccacc Update launch.sh 2024-10-11 11:21:49 -07:00
SpudGunMan
0d43243650 Update launch.sh 2024-10-11 11:20:40 -07:00
SpudGunMan
bb0088171d Update locationdata.py 2024-10-10 19:03:08 -07:00
SpudGunMan
8f74b19aae Delete mesh_network_analyzer.py 2024-10-10 18:20:38 -07:00
Kelly
62a9c84114 Merge pull request #83 from SpudGunMan/lab
Lab
2024-10-10 18:15:41 -07:00
SpudGunMan
a863def55a Update README.md 2024-10-10 18:14:12 -07:00
SpudGunMan
59d538cdee Update README.md 2024-10-10 18:09:18 -07:00
SpudGunMan
8aad0c71f7 Update README.md 2024-10-10 18:08:25 -07:00
SpudGunMan
de1b5f08b1 Update README.md 2024-10-10 18:05:37 -07:00
Kelly
75217eea04 Merge pull request #81 from SpudGunMan/lab
v1.5
2024-10-10 17:51:55 -07:00
SpudGunMan
9d37a9eaa5 finesse
to enhance fixes and fix enhances
2024-10-10 17:47:45 -07:00
SpudGunMan
66d7f98716 fix 2024-10-10 17:25:40 -07:00
SpudGunMan
ae6dfa3321 cleanupOldCode 2024-10-10 17:18:05 -07:00
SpudGunMan
3f4c6f8703 shame 2024-10-10 12:02:38 -07:00
SpudGunMan
eb96d22139 Update system.py 2024-10-10 11:55:42 -07:00
SpudGunMan
448b30e0f2 Update mesh_bot.py 2024-10-10 11:54:41 -07:00
SpudGunMan
110909f64d Update mesh_bot.py 2024-10-10 10:37:45 -07:00
SpudGunMan
58b25f6da4 Update mesh_bot.py 2024-10-10 10:19:11 -07:00
SpudGunMan
91110460fb Update llm.py 2024-10-10 01:33:56 -07:00
SpudGunMan
a81fafe268 Update llm.py 2024-10-10 00:11:01 -07:00
SpudGunMan
f5512b19da Update llm.py 2024-10-10 00:00:33 -07:00
SpudGunMan
6da457bb06 Update llm.py 2024-10-09 23:43:44 -07:00
SpudGunMan
3117a9d4ea Update llm.py 2024-10-09 23:40:27 -07:00
SpudGunMan
8ffcc18c62 ragTest 2024-10-09 23:29:32 -07:00
SpudGunMan
81364738cf Update llm.py 2024-10-09 22:51:40 -07:00
SpudGunMan
ce9436f449 Update llm.py 2024-10-09 22:17:04 -07:00
SpudGunMan
bb83a64317 Update README.md 2024-10-09 22:16:24 -07:00
SpudGunMan
a4bd6f7b83 Update llm.py 2024-10-09 22:16:20 -07:00
SpudGunMan
83300d0ef1 better to keep these 2024-10-09 22:08:42 -07:00
SpudGunMan
6a6c10b825 Update README.md 2024-10-09 18:07:05 -07:00
SpudGunMan
cb398c5b1d Update report_generator.py 2024-10-09 17:57:07 -07:00
SpudGunMan
68893741f0 Update report_generator5.py 2024-10-09 17:56:12 -07:00
SpudGunMan
64dede5c9a Update llm.py 2024-10-09 17:40:11 -07:00
SpudGunMan
3f920aaf60 LLM15
@xdep thanks for confirming this method works.
enhanced with Ollama Client for URL access to localhost or beyond. Enhance for pi in the sky use!
Also refactored the history and the template got some fine tuning. trimmed off the langchan includes as well!
2024-10-09 17:30:09 -07:00
SpudGunMan
749296a6c0 Update llm.py 2024-10-09 10:48:49 -07:00
SpudGunMan
709e5a9949 Merge branch 'lab' of https://github.com/SpudGunMan/meshing-around into lab 2024-10-09 00:30:40 -07:00
SpudGunMan
5d35b5fb2c cleanerLogic? 2024-10-09 00:30:37 -07:00
Kelly
3059b88991 Update mmind.py 2024-10-09 00:05:39 -07:00
SpudGunMan
0fb0b4df0a Update joke.py 2024-10-08 23:47:45 -07:00
SpudGunMan
26df351edf Update mesh_bot.py 2024-10-08 23:23:57 -07:00
SpudGunMan
6e4979df44 fixes 2024-10-08 23:22:38 -07:00
SpudGunMan
55a681e7f2 Update mesh_bot.py 2024-10-08 23:16:00 -07:00
SpudGunMan
4388b6512e bugs 2024-10-08 23:14:26 -07:00
SpudGunMan
efd6b882d1 Update mesh_bot.py 2024-10-08 23:00:34 -07:00
SpudGunMan
9f629c6ec6 Update mesh_bot.py 2024-10-08 22:54:38 -07:00
SpudGunMan
5aae7e9eeb bust 2024-10-08 22:48:39 -07:00
SpudGunMan
165ff43df4 fixGame 2024-10-08 22:47:40 -07:00
SpudGunMan
0fe5640b23 fixInt2 2024-10-08 22:07:26 -07:00
SpudGunMan
780d52a444 int2 2024-10-08 21:21:50 -07:00
SpudGunMan
de46e0c679 Update system.py 2024-10-08 21:13:12 -07:00
SpudGunMan
200bee3721 Update mesh_bot.py 2024-10-08 20:59:06 -07:00
SpudGunMan
c7b4bce1aa Update mesh_bot.py 2024-10-08 20:54:47 -07:00
SpudGunMan
af2d6c4f97 Update system.py 2024-10-08 20:52:51 -07:00
SpudGunMan
6d71e97590 Update mesh_bot.py 2024-10-08 20:45:01 -07:00
SpudGunMan
c3a4ac781a Update mesh_bot.py 2024-10-08 20:41:32 -07:00
SpudGunMan
70a9eb4b93 enableOllamaClient
dev mode only for now
2024-10-08 20:34:22 -07:00
SpudGunMan
f2e06bcd50 Update mesh_bot.py 2024-10-08 18:19:14 -07:00
SpudGunMan
416e6951fc Update system.py 2024-10-08 18:19:08 -07:00
SpudGunMan
4e608ad268 Update system.py 2024-10-08 18:03:57 -07:00
SpudGunMan
580f3dd308 Update system.py 2024-10-08 17:52:37 -07:00
SpudGunMan
66f3c75b40 Update joke.py 2024-10-08 17:48:11 -07:00
SpudGunMan
66d2593a1c Update joke.py 2024-10-08 17:42:48 -07:00
SpudGunMan
7473148a8f Update system.py 2024-10-08 17:37:29 -07:00
SpudGunMan
f748967669 Update system.py 2024-10-08 17:36:00 -07:00
SpudGunMan
f922884f04 Update joke.py 2024-10-08 17:33:41 -07:00
SpudGunMan
659c19c3bd Update joke.py 2024-10-08 17:17:03 -07:00
SpudGunMan
15a0920943 Update system.py 2024-10-08 16:57:03 -07:00
SpudGunMan
2ae634b434 Update joke.py 2024-10-08 16:54:57 -07:00
SpudGunMan
64dff2c1cc Update system.py 2024-10-08 16:49:15 -07:00
SpudGunMan
8354372ffb enhance properly 2024-10-08 16:44:50 -07:00
SpudGunMan
1b1861c7a2 fix 2024-10-08 16:39:49 -07:00
SpudGunMan
f3dc72b234 Update joke.py 2024-10-08 16:21:49 -07:00
SpudGunMan
eac0700338 Update system.py 2024-10-08 16:18:16 -07:00
SpudGunMan
d75ad73d63 DadJokes Enhanced!
added word2Emoji
2024-10-08 16:10:19 -07:00
SpudGunMan
8b72c6d21d Update system.py 2024-10-08 14:28:26 -07:00
SpudGunMan
a3185a5a67 Update system.py 2024-10-08 13:38:08 -07:00
SpudGunMan
e7c9674fb4 mowarTELEMETRY 2024-10-08 13:31:37 -07:00
SpudGunMan
a56b64fd63 default1Day 2024-10-08 12:58:51 -07:00
SpudGunMan
c4dcd44efb fixTelemetryval 2024-10-08 02:37:11 -07:00
SpudGunMan
2b90234905 Update system.py 2024-10-08 02:28:32 -07:00
SpudGunMan
336952bc57 Update system.py 2024-10-08 02:26:16 -07:00
SpudGunMan
7d2ff25b05 Update system.py 2024-10-08 01:47:35 -07:00
SpudGunMan
b3a1ac2e1e Update mesh_bot.py 2024-10-08 00:54:52 -07:00
SpudGunMan
e455da63c2 telemetry 2024-10-08 00:47:16 -07:00
SpudGunMan
d49da4376a Update launch.sh 2024-10-08 00:43:29 -07:00
SpudGunMan
ab6f072b1c Update launch.sh 2024-10-08 00:37:29 -07:00
SpudGunMan
a5ab1aebf3 here is what I wanted 2024-10-08 00:17:03 -07:00
SpudGunMan
ef18f1cb0c ooof 2024-10-08 00:11:27 -07:00
SpudGunMan
639568f9a0 Update launch.sh 2024-10-08 00:10:06 -07:00
SpudGunMan
bc458d7907 Update .gitignore 2024-10-08 00:00:49 -07:00
SpudGunMan
f843ee1f27 Update .gitignore 2024-10-08 00:00:38 -07:00
SpudGunMan
5d3cb8c1f3 Update install.sh 2024-10-07 23:17:32 -07:00
SpudGunMan
9df35bd9eb Update launch.sh 2024-10-07 23:13:59 -07:00
SpudGunMan
0619429c00 Update launch.sh 2024-10-07 23:13:48 -07:00
SpudGunMan
a5f68e1c20 telemetryStill 2024-10-07 22:58:27 -07:00
SpudGunMan
8c32714dfb Update system.py 2024-10-07 21:34:56 -07:00
SpudGunMan
dc561a8d17 Update system.py 2024-10-07 21:01:00 -07:00
SpudGunMan
e2cb809961 Update mesh_bot.py 2024-10-07 20:54:31 -07:00
SpudGunMan
fe6d7fa663 Update mesh_bot.py 2024-10-07 18:10:34 -07:00
SpudGunMan
3f3a4ea4e3 Update mesh_bot.py 2024-10-07 12:41:21 -07:00
SpudGunMan
6df93c3436 Update system.py 2024-10-07 12:04:56 -07:00
SpudGunMan
1dd8c4a102 Merge branch 'lab' of https://github.com/SpudGunMan/meshing-around into lab 2024-10-07 12:03:31 -07:00
SpudGunMan
14d8197b1e fixTelemetry
forgot to test int2
2024-10-07 12:03:15 -07:00
Kelly
828970cc5e Update README.md 2024-10-07 10:39:09 -07:00
Kelly
d9f596f06d Update README.md 2024-10-07 10:35:46 -07:00
SpudGunMan
0274f96d6c Woah🐎 2024-10-07 02:56:59 -07:00
SpudGunMan
a1e108ca5e WOAH 2024-10-07 02:39:07 -07:00
SpudGunMan
6a8a258dc0 Update system.py 2024-10-07 02:21:23 -07:00
SpudGunMan
92db01c4fe woah 2024-10-07 02:17:54 -07:00
SpudGunMan
208af67a50 refactoringCHUNKER
@Nestpebble FYI
2024-10-07 01:43:19 -07:00
SpudGunMan
2aad28cd7f Update system.py 2024-10-07 01:16:27 -07:00
SpudGunMan
b3959e0867 Update README.md 2024-10-06 22:45:53 -07:00
SpudGunMan
b0dca67a70 repeaterList
done?
2024-10-06 22:44:17 -07:00
SpudGunMan
2cd0ff81e4 Update locationdata.py 2024-10-06 22:37:31 -07:00
SpudGunMan
d1a39153b3 Update locationdata.py 2024-10-06 22:35:42 -07:00
SpudGunMan
85a29a6942 Update locationdata.py 2024-10-06 22:27:37 -07:00
SpudGunMan
22d8b889fe repeaterList
enhancment
2024-10-06 22:24:31 -07:00
SpudGunMan
62bddf8b34 Update locationdata.py 2024-10-06 20:39:44 -07:00
SpudGunMan
75d7783e20 Update locationdata.py 2024-10-06 20:38:10 -07:00
SpudGunMan
93cfbc8230 repeaterlist 2024-10-06 20:34:32 -07:00
SpudGunMan
c18d56dac5 Update system.py 2024-10-06 18:34:46 -07:00
SpudGunMan
775c54566c Update locationdata.py 2024-10-06 18:08:26 -07:00
SpudGunMan
94d5c4c325 Update simulator.py 2024-10-06 17:19:02 -07:00
SpudGunMan
2c57d3aacf fix 2024-10-06 17:10:11 -07:00
SpudGunMan
71dd28e09d Update mesh_bot.py 2024-10-06 17:05:54 -07:00
SpudGunMan
ee01373b27 Update system.py 2024-10-06 16:58:00 -07:00
SpudGunMan
2eeab7a55e Update system.py 2024-10-06 16:56:56 -07:00
SpudGunMan
67d326c6c7 Update system.py 2024-10-06 16:53:09 -07:00
SpudGunMan
7af52572e0 enhance 2024-10-06 16:50:38 -07:00
SpudGunMan
4f6d74a19d fix 2024-10-06 16:48:33 -07:00
SpudGunMan
c3232437af ooof 2024-10-06 16:39:12 -07:00
SpudGunMan
cf4b51a297 fixes 2024-10-06 16:35:01 -07:00
SpudGunMan
124d5af2f2 hunting 2024-10-06 16:27:26 -07:00
SpudGunMan
a0eb4a6a6e Update system.py 2024-10-06 16:12:53 -07:00
SpudGunMan
2f560a9049 enhanceTelemetry 2024-10-06 16:11:05 -07:00
SpudGunMan
d9c250b9eb Update README.md 2024-10-06 14:10:29 -07:00
SpudGunMan
e67d9903ac Update README.md 2024-10-06 14:06:45 -07:00
SpudGunMan
6b4ca9b0cd Update README.md 2024-10-06 14:03:34 -07:00
SpudGunMan
5eab13ee0c Update README.md 2024-10-06 13:59:51 -07:00
SpudGunMan
3fe19ab837 Update README.md 2024-10-06 13:59:30 -07:00
SpudGunMan
b61161f257 Update report_generator5.py 2024-10-06 02:33:57 -07:00
SpudGunMan
9ed65acfb1 fixNoLocBug 2024-10-06 02:31:47 -07:00
SpudGunMan
0404a7444c Update report_generator5.py 2024-10-06 02:16:28 -07:00
SpudGunMan
34b2e7af71 Update report_generator.py 2024-10-06 02:16:14 -07:00
SpudGunMan
6f88d4fa1d stuff 2024-10-06 02:12:23 -07:00
SpudGunMan
d486aa73e0 Update locationdata.py 2024-10-06 01:56:57 -07:00
SpudGunMan
f447d8ad55 Update system.py 2024-10-06 01:32:54 -07:00
SpudGunMan
9cd25db5c1 Update system.py 2024-10-06 01:30:35 -07:00
SpudGunMan
c198bd30a5 Update system.py 2024-10-06 01:27:58 -07:00
SpudGunMan
964db770d6 Update system.py 2024-10-06 01:16:34 -07:00
SpudGunMan
a906aaf56b Update system.py 2024-10-06 00:53:48 -07:00
SpudGunMan
0ebac22842 Update system.py 2024-10-06 00:52:48 -07:00
SpudGunMan
c93aee580f mowarTelemetry 2024-10-06 00:20:27 -07:00
SpudGunMan
329fafea47 telelmetry 2024-10-05 22:14:49 -07:00
SpudGunMan
c4b5022f45 clean 2024-10-05 22:07:36 -07:00
SpudGunMan
f31416c7cb Telemetry
new telemetry line
2024-10-05 21:28:35 -07:00
SpudGunMan
7267542db9 enhance 2024-10-05 20:57:57 -07:00
SpudGunMan
ff4eab235d ffs 2024-10-05 20:55:00 -07:00
SpudGunMan
e8caf704fe cleanup 2024-10-05 20:49:22 -07:00
SpudGunMan
5d6f1a9f95 new📁 2024-10-05 20:11:47 -07:00
SpudGunMan
726cd7d4d2 woahLottaRefactoring 2024-10-05 20:08:33 -07:00
SpudGunMan
34077c6818 betterUserDetection 2024-10-05 16:09:04 -07:00
SpudGunMan
5434228df0 Update llm.py 2024-10-05 15:59:21 -07:00
SpudGunMan
b28593c46a fixShameWall 2024-10-05 15:37:20 -07:00
SpudGunMan
1502ccf3ac enhance? 2024-10-05 15:31:41 -07:00
SpudGunMan
8f47bc5799 fixPegBug
this has been bothering me
2024-10-05 02:40:02 -07:00
SpudGunMan
ea0c06444a Update README.md 2024-10-05 02:01:31 -07:00
SpudGunMan
9d42f856d1 enhance with config settings 2024-10-05 01:58:02 -07:00
SpudGunMan
e0c6e9313b Update README.md 2024-10-05 01:21:14 -07:00
SpudGunMan
a01ce204ab ffs 2024-10-05 01:18:56 -07:00
SpudGunMan
194019c091 Update README.md 2024-10-05 01:17:48 -07:00
SpudGunMan
99884884de Update README.md 2024-10-05 01:17:20 -07:00
SpudGunMan
ea437036db Update README.md 2024-10-05 01:17:05 -07:00
SpudGunMan
1eb0b4fda5 stuff 2024-10-05 01:14:29 -07:00
SpudGunMan
484b965bbc Update README.md 2024-10-05 00:58:38 -07:00
SpudGunMan
1ee73008be Update meshtrekker.py 2024-10-05 00:41:55 -07:00
SpudGunMan
3c38357ca9 enhance
add config.ini
2024-10-05 00:21:58 -07:00
SpudGunMan
92f9fd4e69 Enhance
new found items game play
2024-10-04 23:41:44 -07:00
SpudGunMan
9c9b090af4 make other list nice 2024-10-04 22:06:27 -07:00
SpudGunMan
53e4e0c59b enhance list 2024-10-04 22:03:45 -07:00
SpudGunMan
df446ccab5 reverse 2024-10-04 21:41:50 -07:00
SpudGunMan
0d7ea454b6 fix 2024-10-04 21:39:52 -07:00
SpudGunMan
d57dfb1055 enhance 2024-10-04 21:38:32 -07:00
SpudGunMan
99b4e5a1b5 docs 2024-10-04 21:03:58 -07:00
SpudGunMan
c42b6cccb1 🕹️📊 2024-10-04 20:33:48 -07:00
SpudGunMan
f24a04d0dc fix 2024-10-04 19:58:59 -07:00
SpudGunMan
8373c4b3c5 fixErrorsLogs 2024-10-04 19:45:48 -07:00
SpudGunMan
08d7171d1a Update report_generator5.py 2024-10-04 18:49:47 -07:00
SpudGunMan
ede455f2e3 Update videopoker.py 2024-10-04 17:37:18 -07:00
SpudGunMan
b63981286c Update videopoker.py 2024-10-04 17:34:33 -07:00
SpudGunMan
b9dfa38bcc Update settings.py 2024-10-04 17:31:58 -07:00
SpudGunMan
fe9c63387b 🐰🐰 2024-10-04 17:31:37 -07:00
SpudGunMan
ee49f69231 🐰
hop count limit for games to keep network use ok
2024-10-04 17:23:00 -07:00
SpudGunMan
94907bec26 CQCQ📻 2024-10-04 17:15:02 -07:00
SpudGunMan
3566016f7b Update bbstools.py 2024-10-04 17:05:19 -07:00
SpudGunMan
182b725f43 serviceFiles 2024-10-04 17:05:15 -07:00
SpudGunMan
6af486d772 Update meshtrekker.py 2024-10-04 15:05:46 -07:00
SpudGunMan
18db0b0028 🥔
cleanup and such
2024-10-04 15:04:12 -07:00
SpudGunMan
d95962a358 HTML5 2024-10-04 14:55:28 -07:00
SpudGunMan
983bf4d61f Update report_generator.py 2024-10-04 14:54:17 -07:00
SpudGunMan
0928d8da0d tidyUp
@xdep I did some cleanup of CSS I cant figure out the body text color not being applied it seems to be fine but  im stuck for now
2024-10-04 14:52:28 -07:00
SpudGunMan
ae6c94da97 Update report_generator5.py 2024-10-04 14:44:06 -07:00
SpudGunMan
35492ca88b Update report_generator5.py 2024-10-04 14:38:38 -07:00
SpudGunMan
b94029e925 Update report_generator5.py 2024-10-04 14:31:12 -07:00
SpudGunMan
aa643610b5 Update report_generator5.py 2024-10-04 14:22:53 -07:00
SpudGunMan
573d9ec036 Update report_generator5.py 2024-10-04 14:20:49 -07:00
SpudGunMan
c84906f2ce Update report_generator5.py 2024-10-04 14:10:30 -07:00
SpudGunMan
96ac640116 Update report_generator5.py 2024-10-04 14:09:49 -07:00
SpudGunMan
7bc93f8a79 Update report_generator5.py 2024-10-04 14:04:47 -07:00
SpudGunMan
a2e3c59857 Update report_generator5.py 2024-10-04 13:54:14 -07:00
SpudGunMan
1306b25950 enhanceHTMLcss 2024-10-04 13:51:56 -07:00
Kelly
d52956ebf6 Merge pull request #80 from xdep/patch-3 2024-10-04 09:29:44 -07:00
Device
dd22f41fd0 Create meshtrekker.py
This game is a idea and placeholder as well a start to further implement it into the meshing-around bot setup.

Kelly, the idea is cool, but needs some work. and as well a implementation for the dashboard with maybe a seperated map to track players there movement.  But let's figure this out later hehe

Enjoy the draft!
2024-10-04 15:13:55 +02:00
SpudGunMan
f0df305124 Update mesh_network_analyzer.py 2024-10-04 03:29:29 -07:00
SpudGunMan
073f13b44b use lab branch
use the lab branch for this file use
2024-10-04 03:28:14 -07:00
Kelly
d6418c941a Merge pull request #79 from xdep/patch-1 2024-10-04 03:24:40 -07:00
SpudGunMan
3adcafde2b HTML5
@xdep I got it converted I am sleepy but if you work on anything switch to this fork it has a LOT of changes for this tool
2024-10-04 02:44:25 -07:00
SpudGunMan
6151daa68b Update mna.py 2024-10-04 02:42:26 -07:00
SpudGunMan
89bd5727a1 Update mna.py 2024-10-04 02:41:45 -07:00
SpudGunMan
31ea9d8c55 Update mna.py 2024-10-04 02:34:30 -07:00
SpudGunMan
6d8e901158 Update mna.py 2024-10-04 02:32:44 -07:00
SpudGunMan
1c957ba4d9 Update mna.py 2024-10-04 02:27:14 -07:00
SpudGunMan
a2d9049541 Update mna.py 2024-10-04 02:24:37 -07:00
SpudGunMan
93ba9615d9 Update mna.py 2024-10-04 02:22:41 -07:00
SpudGunMan
1a165b36ae Update mna.py 2024-10-04 02:19:52 -07:00
SpudGunMan
cbc3cb8464 Update mna.py 2024-10-04 02:18:24 -07:00
SpudGunMan
dad7cccae0 Create mna.py 2024-10-04 02:17:29 -07:00
SpudGunMan
f14370ac8d Delete mesh_network_analyzer.py 2024-10-04 02:14:53 -07:00
SpudGunMan
783dcad5c0 Create mesh_network_analyzer.py 2024-10-04 01:45:37 -07:00
SpudGunMan
014a45a2dc Update report_generator.py 2024-10-04 01:18:50 -07:00
SpudGunMan
24e93e3bd5 Update report_generator.py 2024-10-04 01:15:45 -07:00
Device
5ae16e6de1 Update mesh_network_analyzer.py
Added a more repsonsive design
Added light/dark mode 
Added html5 structure with matching css
2024-10-04 10:06:41 +02:00
SpudGunMan
195e6327e0 enhance 2024-10-04 00:55:25 -07:00
SpudGunMan
3649246d19 Update report_generator.py 2024-10-04 00:34:49 -07:00
SpudGunMan
7044f85245 Update report_generator.py 2024-10-04 00:31:12 -07:00
SpudGunMan
fcf8fb5846 enhance 2024-10-04 00:28:28 -07:00
SpudGunMan
cd9ab37d00 Update report_generator.py 2024-10-03 23:53:31 -07:00
SpudGunMan
c607913e82 logClassify 2024-10-03 23:49:34 -07:00
SpudGunMan
919b5e730c enhance 2024-10-03 23:45:44 -07:00
SpudGunMan
4fbc0f5817 Update report_generator.py 2024-10-03 23:11:01 -07:00
SpudGunMan
afd1bcae17 enhance 2024-10-03 23:00:34 -07:00
SpudGunMan
1334763e3d Update report_generator.py 2024-10-03 22:42:22 -07:00
SpudGunMan
c6725395da cleanup 2024-10-03 22:30:16 -07:00
SpudGunMan
617ca3ecbc Update report_generator.py 2024-10-03 22:21:55 -07:00
SpudGunMan
bdd376c46d Update report_generator.py 2024-10-03 21:58:12 -07:00
SpudGunMan
8e6f126335 report_generator 2024-10-03 21:50:34 -07:00
SpudGunMan
70af483e01 wall of shame!
@xdep dude seriously thank you so much fun
2024-10-03 21:38:30 -07:00
SpudGunMan
e1331d7fd5 Update mesh_network_analyzer.py 2024-10-03 21:22:08 -07:00
SpudGunMan
6cec06a14e Update mesh_network_analyzer.py 2024-10-03 21:01:54 -07:00
SpudGunMan
1d32ab9ee7 Update mesh_network_analyzer.py 2024-10-03 20:44:07 -07:00
SpudGunMan
9035be3f5d enhance
@xdep I am now geared up to add more data. also of note I removed ASCII escape's from the flat log to clean up the output for parsing so much fun thank you!
2024-10-03 19:53:13 -07:00
SpudGunMan
cf127ae0e7 Update system.py 2024-10-03 19:38:13 -07:00
SpudGunMan
7b940c409f ClearLogs 2024-10-03 19:04:56 -07:00
SpudGunMan
02e6225240 Update mesh_network_analyzer.py 2024-10-03 17:55:55 -07:00
SpudGunMan
97660d7b2d Update mesh_bot.py
ok enough
2024-10-03 16:41:27 -07:00
SpudGunMan
b53c0cfe4f more 🐄 2024-10-03 16:38:49 -07:00
SpudGunMan
b676ace34b 🛎️
respond to bell command
2024-10-03 16:34:24 -07:00
SpudGunMan
2d1e9af5cb 📍
position exchange response
2024-10-03 16:24:29 -07:00
SpudGunMan
679184e8e0 Update README.md 2024-10-03 15:52:46 -07:00
SpudGunMan
7f912e04e7 Update mesh_network_analyzer.py 2024-10-03 15:08:20 -07:00
SpudGunMan
26ed32d51f updateLogName 2024-10-03 14:43:44 -07:00
SpudGunMan
374d76bb35 Update mesh_bot.py 2024-10-03 14:35:19 -07:00
SpudGunMan
664c3f1277 enhanceError 2024-10-03 14:29:06 -07:00
SpudGunMan
c48c89b0d9 Update settings.py 2024-10-03 13:09:47 -07:00
SpudGunMan
7f1787e52b 📊 2024-10-03 12:43:03 -07:00
SpudGunMan
a28f51fa55 Update README.md 2024-10-03 11:10:32 -07:00
SpudGunMan
383102802b Create mesh_bot_w3.tmp 2024-10-03 11:03:38 -07:00
SpudGunMan
0516749708 UnoPing
Enhance with a new uno game which might be multiplayer and auto-ping
2024-10-03 11:00:10 -07:00
Kelly
002f1570dc Merge pull request #77 from xdep/main
Loganalyser that will output a nice html structure
2024-10-03 10:52:15 -07:00
Kelly
9d025fc3cd Merge pull request #78 from xdep/main
MeshBot Network Analyzer.py
2024-10-03 10:40:56 -07:00
Dev
03bb13b830 Merge branch 'SpudGunMan:main' into main 2024-10-03 13:01:38 +02:00
Dev
b351ed0cf4 Create mesh_network_analyzer.py
Adding a log analyser that will take the logfile and parse it into a html file for monitoring. Run the python script within the logfolder from meshingaround. by default this is sent to /var/www/html/index.html 


See: https://ibb.co/ymz9TxZ
2024-10-03 13:01:21 +02:00
SpudGunMan
abeb28c9cc Update golfsim.py 2024-10-02 23:20:59 -07:00
SpudGunMan
2325f74581 Update simulator.py 2024-10-02 15:13:28 -07:00
SpudGunMan
e75ef3e44d Update simulator.py 2024-10-02 14:51:46 -07:00
SpudGunMan
6431b45769 Update mesh_bot.py 2024-10-02 10:39:48 -07:00
SpudGunMan
7dfcbb619f Update simulator.py 2024-10-02 01:39:07 -07:00
SpudGunMan
051b58ca6f bleEnhance 2024-10-01 17:17:50 -07:00
SpudGunMan
5777b39c22 Update mmind.py 2024-10-01 14:11:38 -07:00
SpudGunMan
eb4f135698 🪣 2024-10-01 11:16:26 -07:00
SpudGunMan
3bcb04ece7 ️HighScore 2024-10-01 00:17:52 -07:00
SpudGunMan
e42aca875f fixStroke 2024-09-30 23:49:56 -07:00
SpudGunMan
28915ab848 Update golfsim.py 2024-09-30 23:25:38 -07:00
SpudGunMan
0fe491871d Update golfsim.py 2024-09-30 23:25:12 -07:00
SpudGunMan
6fc6a483a8 Update golfsim.py 2024-09-30 23:06:27 -07:00
SpudGunMan
13236880b5 Update golfsim.py 2024-09-30 22:56:37 -07:00
SpudGunMan
9461719039 Update golfsim.py 2024-09-30 22:50:31 -07:00
SpudGunMan
63163cc4c1 ️Caddy
added a caddy
2024-09-30 22:28:59 -07:00
SpudGunMan
5cdf159bc1 add TZ local
resolves https://github.com/SpudGunMan/meshing-around/issues/72
2024-09-30 21:17:03 -07:00
SpudGunMan
47a9981fdf enhance
resolve https://github.com/SpudGunMan/meshing-around/issues/73

new game play hazards
2024-09-30 21:16:00 -07:00
SpudGunMan
f85bdb7d02 clubOrder🥪
resolve https://github.com/SpudGunMan/meshing-around/issues/75
2024-09-30 16:56:41 -07:00
SpudGunMan
7796d03e21 Update mmind.py 2024-09-30 16:53:33 -07:00
SpudGunMan
90094c082a Update mmind.py
resolved https://github.com/SpudGunMan/meshing-around/issues/74
2024-09-30 16:53:11 -07:00
SpudGunMan
2b695e2f2e Update wx_meteo.py 2024-09-30 16:43:10 -07:00
SpudGunMan
2e05f3ef64 Update wx_meteo.py
reference
https://github.com/open-meteo/open-meteo/issues/287

and also https://github.com/SpudGunMan/meshing-around/issues/71
2024-09-30 16:39:25 -07:00
SpudGunMan
316f1efd08 Update Dockerfile 2024-09-30 16:18:37 -07:00
SpudGunMan
4678a63955 enhance
expert mode and saves score
2024-09-30 12:56:04 -07:00
SpudGunMan
8584454d5d Update README.md 2024-09-30 09:15:28 -07:00
SpudGunMan
2c6cf76a10 Update mmind.py 2024-09-30 03:17:15 -07:00
SpudGunMan
cd3226df21 enhance 2024-09-30 03:04:38 -07:00
SpudGunMan
4bcc6ef1f2 Update mesh_bot.py 2024-09-30 02:57:46 -07:00
SpudGunMan
77e56c25ae golfEnhance 2024-09-30 02:51:54 -07:00
SpudGunMan
e7b363612a timePlay 2024-09-30 02:44:46 -07:00
SpudGunMan
a217c61ba1 LateNightDoubleFeature
* golfsim
* masterMind

New Games!
2024-09-30 02:37:08 -07:00
SpudGunMan
e7b4fe44c8 Update simulator.py 2024-09-29 21:39:05 -07:00
SpudGunMan
f8cc580b99 Update mesh_bot.py 2024-09-29 15:01:45 -07:00
SpudGunMan
03057b3263 Update mesh_bot.py 2024-09-29 12:55:41 -07:00
SpudGunMan
452b9b7c67 Update mesh_bot.py 2024-09-29 12:50:57 -07:00
SpudGunMan
5ae16d0adc Update videopoker.py 2024-09-29 12:49:14 -07:00
SpudGunMan
5ed135c023 Update README.md 2024-09-28 14:05:03 -07:00
SpudGunMan
d425298cd9 Update dopewar.py 2024-09-28 12:17:16 -07:00
SpudGunMan
786815d073 Update dopewar.py 2024-09-28 12:04:17 -07:00
SpudGunMan
54cad92a3f enhance 💊 game display 2024-09-28 12:03:08 -07:00
SpudGunMan
54e21f4644 Update mesh_bot.py 2024-09-28 00:53:19 -07:00
SpudGunMan
3c76f177cd Update simulator.py 2024-09-28 00:46:47 -07:00
SpudGunMan
aa05c62d94 fix🍋hs 2024-09-28 00:45:57 -07:00
SpudGunMan
3f16158e27 Update mesh_bot.py 2024-09-28 00:28:59 -07:00
SpudGunMan
6f2824512d highscoreEnhance 2024-09-28 00:27:45 -07:00
SpudGunMan
723b67f886 🥔 2024-09-28 00:14:27 -07:00
SpudGunMan
008d55e63b Update mesh_bot.py 2024-09-28 00:12:33 -07:00
SpudGunMan
79885454ab fixLongStandingIssues
fix long time bugs of 21 and the suggestions
2024-09-27 23:47:14 -07:00
SpudGunMan
ba21723bdc fix BlackJackwin! 2024-09-27 23:38:40 -07:00
SpudGunMan
c36c4918a8 Update mesh_bot.py 2024-09-27 22:33:07 -07:00
SpudGunMan
853147518d space 2024-09-27 22:32:46 -07:00
SpudGunMan
2f19d86c95 💩
: get it
2024-09-27 22:17:07 -07:00
SpudGunMan
39bdabffcb Update blackjack.py 2024-09-27 21:57:40 -07:00
SpudGunMan
a7bdaedfe1 Update blackjack.py 2024-09-27 21:57:22 -07:00
SpudGunMan
1c6106081f Weight2Wait
add weight to the wait for responses. as the comment says this is a specific use case for the Q: blah blah A: 2. I am seeing a hang up this is to help keep delivery high.
2024-09-27 21:48:13 -07:00
SpudGunMan
8ab6cded2e 🥝 2024-09-27 21:14:06 -07:00
SpudGunMan
ff63bb959a Update mesh_bot.py 2024-09-27 20:55:50 -07:00
SpudGunMan
6c79bb1ff0 Update mesh_bot.py 2024-09-27 20:50:36 -07:00
SpudGunMan
ce73336c0c Update mesh_bot.py 2024-09-27 20:49:05 -07:00
SpudGunMan
248977c5b5 enhance
* replay hand with H or R
2024-09-27 18:06:11 -07:00
SpudGunMan
77a6e63210 howRandom 2024-09-27 17:39:55 -07:00
SpudGunMan
6f6fb35177 IgnoreDefaultChannel
ability to fully ignore the default channel if wanted, default is false
2024-09-27 17:37:34 -07:00
SpudGunMan
9db565cb4f changeLogPath
tidy up this mess
2024-09-27 17:18:35 -07:00
SpudGunMan
2a254a7fab fixNewTable.pkl 2024-09-27 01:06:15 -07:00
SpudGunMan
15e76ab029 fixCasinoGamesForGood? 2024-09-27 00:57:57 -07:00
SpudGunMan
66b95cdaa0 casinoHighScores
I got a really good score its a shame it was wasted
2024-09-27 00:25:17 -07:00
SpudGunMan
32ea93cb88 Update mesh_bot.py 2024-09-26 22:11:42 -07:00
SpudGunMan
22a2a64861 Update bbstools.py 2024-09-26 21:08:29 -07:00
SpudGunMan
d841fdf02c bbsinfo 2024-09-26 21:03:57 -07:00
Kelly
9421f09ded Merge pull request #69 from SpudGunMan/lab
messages from !hex ID
2024-09-26 20:56:35 -07:00
SpudGunMan
b4af552fb9 Update mesh_bot.py 2024-09-26 20:55:32 -07:00
Kelly
69dfb17460 Merge pull request #68 from Nestpebble/main
Add in BBS-DM via node hex ID
2024-09-26 20:47:09 -07:00
SpudGunMan
4703750c27 Update videopoker.py 2024-09-26 20:45:49 -07:00
Nestpebble
40caf99939 Merge branch 'SpudGunMan:main' into main 2024-09-27 03:43:45 +01:00
Nestpebble
df5f648b26 try and round off adding an interpreter to the bbs DM 2024-09-27 03:30:21 +01:00
Nestpebble
55472bbbc0 try this 2024-09-27 03:12:40 +01:00
Nestpebble
f23c4e2b6a sdf 2024-09-27 02:29:58 +01:00
Nestpebble
0b101d662e trying to simplify nodeid entry... 2024-09-27 02:26:55 +01:00
SpudGunMan
a7f07afc14 consolidateTime 2024-09-26 18:13:58 -07:00
SpudGunMan
2715021898 Update README.md 2024-09-26 18:02:52 -07:00
SpudGunMan
e8fa0036e2 Update mesh_bot.py 2024-09-26 17:57:05 -07:00
SpudGunMan
f628a5e7ef Update mesh_bot.py 2024-09-26 17:56:56 -07:00
SpudGunMan
95e6bc120e Update mesh_bot.py 2024-09-26 17:32:34 -07:00
SpudGunMan
0e35c891c4 Update mesh_bot.py 2024-09-26 17:29:54 -07:00
SpudGunMan
b7a3e7014c Update mesh_bot.py 2024-09-26 17:26:42 -07:00
Nestpebble
0c1c587bc7 strip ! from tonode, cos its copied by default on
the android app.
2024-09-27 01:26:01 +01:00
SpudGunMan
a0a2c60e63 Update mesh_bot.py 2024-09-26 17:25:29 -07:00
SpudGunMan
45c912a0d6 Update mesh_bot.py 2024-09-26 17:19:50 -07:00
Kelly
39945f161d Merge pull request #66 from Nestpebble/main
Help messages, condensed ping
2024-09-26 17:15:46 -07:00
Nestpebble
ed958302bd sdfsdfsdf 2024-09-26 23:07:19 +01:00
Nestpebble
477f2141d7 sdfsdf 2024-09-26 22:57:20 +01:00
Nestpebble
d321a958f0 more tidying 2024-09-26 22:50:45 +01:00
Nestpebble
d14f1df823 tweak some help messages 2024-09-26 22:49:20 +01:00
Nestpebble
f7e3b9f6c7 Merge branch 'SpudGunMan:main' into main 2024-09-26 22:25:20 +01:00
Nestpebble
cd3ac201f8 general tidy up, done 2024-09-26 22:23:15 +01:00
Nestpebble
ceef493b01 dfgdf 2024-09-26 22:17:38 +01:00
Nestpebble
480a75e30c Merge branch 'main' of https://github.com/Nestpebble/meshing-around 2024-09-26 22:17:02 +01:00
Nestpebble
d8cc953fe7 Removed handle_testing & handle_ack,
now in handle_ping
2024-09-26 22:16:17 +01:00
Nestpebble
0baec88321 Removed handle_testing & handle_ack,
now in handle_ping
2024-09-26 22:12:10 +01:00
Nestpebble
74bd3f681f dssdf 2024-09-26 22:05:31 +01:00
Nestpebble
713b750f4a sdfsd 2024-09-26 21:56:41 +01:00
Nestpebble
11eee911ca sdfsdf 2024-09-26 21:54:47 +01:00
Nestpebble
b288aaea90 szadsad 2024-09-26 21:32:25 +01:00
Nestpebble
7acc018fd2 sdfsd 2024-09-26 21:30:35 +01:00
Nestpebble
7aba1096f9 szd 2024-09-26 21:20:30 +01:00
Nestpebble
0be7202144 dsfrg 2024-09-26 21:18:49 +01:00
Nestpebble
83a5db74e5 Merge branch 'main' of https://github.com/Nestpebble/meshing-around 2024-09-26 21:16:20 +01:00
Nestpebble
8dc4371beb deghdfgdrfgdf 2024-09-26 21:16:08 +01:00
Nestpebble
e5045a0984 drfgdf 2024-09-26 21:14:29 +01:00
Nestpebble
2c9b37a0cc sdfsdfsdf 2024-09-26 21:09:53 +01:00
Nestpebble
b608482220 typo 2024-09-26 21:05:11 +01:00
Nestpebble
9290fac899 I think I consolidated the pings... 2024-09-26 21:03:43 +01:00
Nestpebble
d7901ee575 sdfsdfsd 2024-09-26 18:57:55 +01:00
Nestpebble
7eb33a5aef sdfsdfsdf 2024-09-26 18:56:36 +01:00
Nestpebble
c5dc103ac0 compressed ping response 2024-09-26 15:28:48 +01:00
Nestpebble
c90172a862 fixed it 2024-09-26 14:31:58 +01:00
Nestpebble
8540786c2c added help text to history 2024-09-26 14:28:49 +01:00
Nestpebble
7aeb8e851d Update mesh_bot.py 2024-09-26 14:24:27 +01:00
Nestpebble
2f207dc3d9 Update mesh_bot.py 2024-09-26 14:18:25 +01:00
Nestpebble
7e2be73962 Update mesh_bot.py 2024-09-26 14:17:29 +01:00
SpudGunMan
e2a87eb945 ReticulatingSplines
Reticulating some Splines and other items which need full Reticulation.
2024-09-26 00:18:07 -07:00
SpudGunMan
4b79c6304f Update install.sh 2024-09-25 23:00:02 -07:00
SpudGunMan
22f63e1056 cleanup 2024-09-25 22:56:01 -07:00
SpudGunMan
bf1613ba66 Update mesh_bot.py 2024-09-25 22:40:26 -07:00
SpudGunMan
38e04db0cb Update install.sh 2024-09-25 22:38:45 -07:00
SpudGunMan
08cc3034a2 Update install.sh 2024-09-25 22:37:13 -07:00
SpudGunMan
a0eb176b6e Update install.sh 2024-09-25 22:33:32 -07:00
SpudGunMan
a43774b33f Update install.sh 2024-09-25 22:32:01 -07:00
SpudGunMan
8369b4d205 Update install.sh 2024-09-25 22:26:37 -07:00
SpudGunMan
87e51cf9f9 Update install.sh 2024-09-25 22:24:08 -07:00
SpudGunMan
8a8fb79fde Update install.sh 2024-09-25 22:19:36 -07:00
SpudGunMan
1dc4cf36b1 Update install.sh 2024-09-25 22:16:13 -07:00
SpudGunMan
75cf43e02a enhance 2024-09-25 21:58:34 -07:00
SpudGunMan
dd8357453b serviceFix 2024-09-25 21:43:04 -07:00
SpudGunMan
e9b273a62c Update system.py 2024-09-25 21:23:12 -07:00
SpudGunMan
ebebd0fda6 Update install.sh 2024-09-25 21:09:29 -07:00
SpudGunMan
3e06902b07 Update config.template 2024-09-25 21:09:11 -07:00
SpudGunMan
a874b42c41 Update install.sh 2024-09-25 19:27:30 -07:00
Nestpebble
f6215d3563 Tidied up, added a few help messages. 2024-09-26 02:02:26 +01:00
SpudGunMan
4fad58f7fe Update system.py 2024-09-25 17:12:05 -07:00
SpudGunMan
276e4c3d09 ? trap 2024-09-25 16:38:27 -07:00
SpudGunMan
8aef4d605b Update README.md 2024-09-25 15:42:59 -07:00
SpudGunMan
962c891baa fixes 2024-09-25 15:40:50 -07:00
Kelly
eb596ea901 Merge pull request #65 from SpudGunMan/lab
enhance CMD Logic with isDM
2024-09-25 14:30:20 -07:00
SpudGunMan
e708ec9adc Update mesh_bot.py 2024-09-25 14:27:39 -07:00
Kelly
c631a083ea Merge pull request #64 from Nestpebble/main
Nestpebble isDM
2024-09-25 13:36:29 -07:00
Nestpebble
169ea8c233 Update mesh_bot.py 2024-09-25 13:12:08 +01:00
Nestpebble
241e2258e8 Update mesh_bot.py 2024-09-25 13:04:08 +01:00
Nestpebble
5d2d6bc5fb utilise isDM to split responses 2024-09-25 12:53:42 +01:00
Nestpebble
c7ef22f5c2 Update mesh_bot.py 2024-09-25 12:41:52 +01:00
Nestpebble
3ec89decf0 Update mesh_bot.py 2024-09-25 12:36:39 +01:00
SpudGunMan
2693f47ed5 Update locationdata.py 2024-09-24 20:01:05 -07:00
SpudGunMan
496a13e0e0 Update mesh_bot.py 2024-09-24 19:33:17 -07:00
SpudGunMan
71ef416dbb Update videopoker.py 2024-09-24 19:32:48 -07:00
SpudGunMan
5bc80c8677 Update dopewar.py 2024-09-24 19:32:44 -07:00
SpudGunMan
8c300f467c Update blackjack.py 2024-09-24 19:32:39 -07:00
SpudGunMan
78b3bf1475 Update bbstools.py 2024-09-24 19:32:32 -07:00
SpudGunMan
ce74f910a7 Update pong_bot.py 2024-09-24 19:32:26 -07:00
SpudGunMan
ece531249e Update bbsdb_admin.py 2024-09-24 19:32:21 -07:00
Nestpebble
5052e2510e Update mesh_bot.py 2024-09-25 01:33:21 +01:00
Nestpebble
b8f8f80499 Update mesh_bot.py 2024-09-25 01:28:47 +01:00
Nestpebble
a13d98e32b Update mesh_bot.py 2024-09-25 01:19:18 +01:00
SpudGunMan
94996dcec8 fixHours 2024-09-24 12:31:44 -07:00
SpudGunMan
4b8ce30df8 sleep1
the short responces of a single letter cause quick turn around
2024-09-24 12:28:51 -07:00
SpudGunMan
485a37e9b5 Update mesh_bot.py 2024-09-23 22:58:36 -07:00
SpudGunMan
b41adb12f7 Update blackjack.py 2024-09-23 22:54:46 -07:00
SpudGunMan
a9cd443bdd Update mesh_bot.py 2024-09-23 21:03:05 -07:00
SpudGunMan
92b9df718f gameplayEnhance 2024-09-23 20:03:34 -07:00
SpudGunMan
a6a4f91d83 Update mesh_bot.py 2024-09-23 19:43:23 -07:00
SpudGunMan
0fb2c498f4 Update mesh_bot.py 2024-09-23 19:43:00 -07:00
SpudGunMan
d87e70f7ee Update mesh_bot.py 2024-09-23 19:35:22 -07:00
SpudGunMan
bee8bae0ae Revert "fixing"
This reverts commit d5ef277121.
2024-09-23 19:29:14 -07:00
SpudGunMan
d5ef277121 fixing 2024-09-23 19:24:14 -07:00
SpudGunMan
dfc8e6c108 Update mesh_bot.py 2024-09-23 19:20:51 -07:00
SpudGunMan
9100aee91f Update mesh_bot.py 2024-09-23 19:10:38 -07:00
SpudGunMan
c94cc92d1c Update mesh_bot.py 2024-09-23 17:41:44 -07:00
SpudGunMan
a41f5e3aca Update blackjack.py 2024-09-23 17:03:43 -07:00
SpudGunMan
cc2d15de0d Update mesh_bot.py 2024-09-23 17:03:40 -07:00
SpudGunMan
39e08aedae borked 2024-09-23 14:43:55 -07:00
SpudGunMan
fe8730fb1f Update mesh_bot.py 2024-09-23 14:42:48 -07:00
SpudGunMan
aac3ac8947 Update mesh_bot.py 2024-09-23 14:42:22 -07:00
SpudGunMan
1acc908bb8 Update mesh_bot.py 2024-09-23 14:33:48 -07:00
SpudGunMan
47ed98a5e1 Update mesh_bot.py 2024-09-23 14:23:07 -07:00
SpudGunMan
103034d1b8 enhance
line 1000
2024-09-23 14:16:26 -07:00
SpudGunMan
608714d00a more enhancing 2024-09-23 13:35:23 -07:00
SpudGunMan
575c287860 unoReverse! 2024-09-23 13:24:45 -07:00
SpudGunMan
820470bef7 tuning 2024-09-23 12:18:23 -07:00
SpudGunMan
ea000ef56c Update mesh_bot.py 2024-09-23 11:22:27 -07:00
SpudGunMan
793b8f8495 Update mesh_bot.py 2024-09-23 01:46:01 -07:00
SpudGunMan
373eee3024 Update mesh_bot.py 2024-09-23 01:43:35 -07:00
SpudGunMan
194273635e kiss 2024-09-23 01:31:24 -07:00
SpudGunMan
0972d7d89d fix 2024-09-23 00:22:58 -07:00
SpudGunMan
d1415f9d86 enhance 2024-09-22 23:56:27 -07:00
SpudGunMan
b3ff3bb406 enhanceHistory
enhance.. enhance...
2024-09-22 23:38:55 -07:00
SpudGunMan
1efce62a8a Update mesh_bot.py 2024-09-22 22:47:28 -07:00
SpudGunMan
442f2ff927 command history feature
fun idea from discord bitflip for command history and showing last time someone used the bot
2024-09-22 22:23:31 -07:00
SpudGunMan
84cefa1be8 Update mesh_bot.py 2024-09-22 18:28:54 -07:00
SpudGunMan
45e9c1eccb Update mesh_bot.py 2024-09-22 18:28:21 -07:00
SpudGunMan
e7e2c9604e 2.5firmware PKI enhance 2024-09-22 18:15:24 -07:00
SpudGunMan
edaf6875ef Update pong_bot.py 2024-09-22 17:45:42 -07:00
SpudGunMan
e7976a0a88 packetDebug 2024-09-22 17:45:33 -07:00
108 changed files with 40913 additions and 2781 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@v5
# 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@28fdb31ff34708d19615a74d67103ddc2ea9725c
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@032a4b3bda1b716928481836ac5bfe36e1feaad6
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@9e436ba9f2d7bcd1d038c8e55d039d37896ddc5d
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@v3
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!"

26
.gitignore vendored
View File

@@ -1,5 +1,7 @@
# config
config.ini
config_new.ini
ini_merge_log.txt
# Pickle files
*.pkl
@@ -7,8 +9,32 @@ config.ini
# virtualenv
venv/
# logs
logs/
install_notes.txt
# modified .service files
etc/*.service
# Python cache
__pycache__/
# rag data
data/rag/*
# qrz db
data/qrz.db
# checklist and inventory databases
data/checklist.db
data/inventory.db
# fileMonitor test file
bee.txt
# .csv files
*.csv
# modules/custom_scheduler.py
modules/custom_scheduler.py

View File

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

View File

@@ -1,18 +1,43 @@
FROM python:3.10-slim
FROM python:3.14-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext && 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
RUN apt-get update && \
apt-get install -y \
build-essential \
python3-dev \
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 /app/requirements.txt
# Copy the rest of the application
COPY . /app
COPY requirements.txt .
COPY config.template /app/config.ini
RUN pip install -r requirements.txt
COPY . .
RUN chmod +x /app/script/docker/entrypoint.sh
COPY config.ini /app/config.ini
COPY entrypoint.sh /app/entrypoint.sh
# Add a non-root user and switch to it
# RUN useradd -m appuser && usermod -a -G dialout appuser
# USER appuser
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
# Expose Meshtastic TCP API port from the host
#EXPOSE 4403
# Meshing Around Web Dashboard port
#EXPOSE 8420
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]

222
INSTALL.md Normal file
View File

@@ -0,0 +1,222 @@
# INSTALL.md
## Table of Contents
- [Manual Install](#manual-install)
- [Docker Installation](#docker-installation)
- [Requirements](#requirements)
- [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 all required dependencies using pip:
```sh
pip install -r requirements.txt
```
Copy the configuration template and edit as needed:
```sh
cp config.template config.ini
```
---
## Docker Installation
See [script/docker/README.md](script/docker/README.md) for Docker-based setup instructions.
Docker is recommended for Windows or if you want an isolated environment.
---
## Requirements
- **Python 3.8 or later** (Python 3.13+ supported in Docker)
- All dependencies are listed in `requirements.txt` and can be installed with:
```sh
pip install -r requirements.txt
```
- To enable emoji in the Debian/Ubuntu console:
```sh
sudo apt-get install fonts-noto-color-emoji
```
- For Ollama LLM support, see the prompts during `install.sh` or visit [https://ollama.com](https://ollama.com).
---
## install.sh
### Purpose
`install.sh` automates installation, configuration, and service setup for the Meshing Around Bot project. It is designed for Linux systems (Debian/Ubuntu/Raspberry Pi and embedded devices).
### Usage
Run from the project root directory:
```sh
bash install.sh
```
To uninstall:
```sh
bash install.sh --nope
```
### What it does
- Checks for existing installations and permissions.
- Optionally moves the project to `/opt/meshing-around`.
- Installs Python and pip if missing (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 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.
## Troubleshooting
### Permissions Issues
If you encounter errors related to file or directory permissions (e.g., "Permission denied" or services failing to start):
- Ensure you are running installation scripts with sufficient privileges (use `sudo` if needed).
- The `logs`, `data`, and `config.ini` files must be owned by the user running the bot (often `meshbot` or your current user).
- You can manually reset permissions using the provided script:
```sh
sudo bash etc/set-permissions.sh meshbot
```
- If you moved the project directory, re-run the permissions script to update ownership.
- For systemd service issues, check logs with:
```sh
sudo journalctl -u mesh_bot.service
```
If problems persist, double-check that the user specified in your service files matches the owner of the project files and directories.

381
README.md
View File

@@ -1,262 +1,185 @@
# meshing-around
Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
# Mesh Bot for Network Testing and BBS Activities
![alt text](etc/pong-bot.jpg "Example Use")
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.
## mesh_bot.sh
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example.
* [Getting Started](#getting-started)
Along with network testing, this bot has a lot of other fun features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM. Or a scheduler to send weather or a reminder weekly for the VHF net.
![Example Use](etc/pong-bot.jpg "Example Use")
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
#### TLDR
* [install.sh](INSTALL.md)
* [Configuration Guide](modules/README.md)
* [Games Help](modules/games/README.md)
Look up data using wiki results, or interact with [Ollama](https://ollama.com) LLM AI see the [OllamaDocs](https://github.com/ollama/ollama/tree/main/docs) If Ollama is enabled you can DM the bot directly. The default model for mesh-bot which is currently `gemma2:2b`
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location.
### Intelligent Keyword Responder
- **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.
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
### Network Tools
- **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.
There is a small collection of games to play like DopeWars, Lemonade Stand, and BlackJack or VideoPoker to name a few, issuing `games` displays help
- **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.
The bot can also be used to monitor a radio frequency and let you know when high SNR RF activity is seen. Using Hamlib(rigctld) to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
### Multi-Radio/Node Support
- **Simultaneous Monitoring**: Observe up to nine networks at once.
- **Flexible Messaging**: Send mail and messages between networks.
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
### Advanced Messaging Capabilities
- **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.
## Full list of commands for the bot
### Interactive AI and Data Lookup
- **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 and Kiwix
- **OpenWebUI, Ollama LLM Integration**: Query the [Ollama](https://github.com/ollama/ollama/tree/main/docs) AI for advanced responses. Supports RAG (Retrieval Augmented Generation) with Wikipedia/Kiwix context and [OpenWebUI](https://github.com/open-webui/open-webui) integration for enhanced AI capabilities. [LLM Readme](modules/llm.md)
- **Satellite Passes**: Find upcoming satellite passes for your location.
- **GeoMeasuring Tools**: Calculate distances and midpoints using collected GPS data; supports Fox & Hound direction finding.
- **RSS & News Feeds**: Receive news and data from multiple sources directly on the mesh.
- Various solar details for radio propagation (spaceWeather module)
- `sun` and `moon` return info on rise and set local time
- `solar` gives an idea of the x-ray flux
- `hfcond` returns a table of HF solar conditions
- Bulletin Board (BBS) functions
- `bbshelp` returns the following
- `bbslist` list the messages by ID and subject
- `bbsread` read a message example use: `bbsread #1`
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message or bbspost @nodeShortName #message`
- `bbsdelete` delete a message example use: `bbsdelete #4`
- Other functions
- `whereami` returns the address of location of sender if known
- `whoami` returns some details of the node asking
- `tide` returns the local tides, NOAA data source
- `wx` and `wxc` returns local weather forecast, (wxc is metric value), NOAA or Open Meteo for weather forecasting.
- `wxa` and `wxalert` return NOAA alerts. Short title or expanded details
- `joke` tells a joke
- `wiki: ` will search wikipedia, return the first few sentances of first result if a match `wiki: lora radio`
- `askai` and `ask:` will ask Ollama LLM AI for a response `askai what temp do I cook chicken`
- `messages` Replay the last messages heard, like Store and Forward
- `motd` or to set the message `motd $New Message Of the day`
- `lheard` returns the last 5 heard nodes with SNR, can also use `sitrep`
- `cmd` returns the list of commands (the help message)
- Games
- `lemonstand` plays the classic Lemonade Stand Finance game via DM
- `dopewars` plays the classic drug trader game via DM
- `blackjack` BlackJack
- `videopoker` Video Poker
### Proximity Alerts
- **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).
## pong_bot.sh
Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
### EAS Alerts
- **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.
- **NINA Alerts (Germany)**: Receive emergency alerts from the xrepository.de feed for Germany.
- **Offline EAS Alerts**: Report EAS alerts over the mesh using external tools, even without internet.
## Hardware
The project is written on Linux on a Pi and should work anywhere [Meshtastic](https://meshtastic.org/docs/software/python/cli/) Python modules will function, with any supported [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. While BLE and TCP will work, they are not as reliable as serial connections.
### File Monitor Alerts
- **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).
## Install
Clone the project with `git clone https://github.com/spudgunman/meshing-around`
code is under a lot of development, so check back often with `git pull`
Copy [config.template](config.template) to `config.ini` and edit for your needs.
`pip install -r requirements.txt`
#### 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.
- **WSJT-X Integration**: Monitor WSJT-X (FT8, FT4, WSPR, etc.) decode messages and forward them to the mesh network with optional callsign filtering.
- **JS8Call Integration**: Monitor JS8Call messages and forward them to the mesh network with optional callsign filtering.
- **Meshages TTS**: The bot can speak mesh messages aloud using [KittenTTS](https://github.com/KittenML/KittenTTS). Enable this feature to have important alerts and messages read out loud on your device—ideal for hands-free operation or accessibility. See [radio.md](modules/radio.md) for setup instructions.
Optionally:
- `install.sh` will automate optional venv and requirements installation.
- `launch.sh` will activate and launch the app in the venv if built.
### Asset Tracking, Check-In/Check-Out, and Inventory Management
Advanced check-in/check-out and asset tracking for people and equipment—ideal for accountability, safety monitoring, and logistics (e.g., Radio-Net, FEMA, trailhead groups). Admin approval workflows, GPS location capture, and overdue alerts. The integrated inventory and point-of-sale (POS) system enables item management, sales tracking, cart-based transactions, and daily reporting, for swaps, emergency supply management, and field operations, maker-places.
For Docker:
Check you have serial port properly shared and the GPU if using LLM with [NVidia](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html)
- `git clone https://github.com/spudgunman/meshing-around`
- `cd meshing-around && docker build -t meshing-around`
- `docker run meshing-around`
### Fun and Games
- **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.
### Configurations
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 BLE mac `meshtastic --ble-scan` **NOTE** I have only tested with a single BLE device and the code is written to only have one interface be a BLE port
#### QuizMaster
- **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**: 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>`.
### Data Reporting
- **HTML Reports**: Visualize bot traffic and data flows with a built-in HTML generator. See [data reporting](logs/README.md) for details.
### Robust Message Handling
- **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, 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
If you dont have git you will need it `sudo apt-get install git`
```sh
git clone https://github.com/spudgunman/meshing-around
```
#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
- **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.
[interface]
type = serial
# port = '/dev/ttyUSB0'
# hostname = 192.168.0.1
# mac = 00:11:22:33:44:55
### Docker Installation
Good for windows or OpenWebUI enabled bots
# Additional interface for dual radio support See config.template for more.
[interface2]
enabled = False
```
The following pair of settings determine how to respond: The default action is to not spam the default channel. Setting'respond_by_DM_only'` will force all messages to be sent to DM, which may not be wanted. Setting the value to False will allow responses in the channel for all to see.
[docker.md](script/docker/README.md)
Setting the default channel is the channel that won't be spammed by the bot. It's the public default channel 0 on the new Meshtastic firmware. Anti-Spam is hard-coded into the responder to prevent abuse of the public channel.
```
## Module Help
Configuration Guide
[modules/README.md](modules/README.md)
### Game Help
Games are DM only by default
[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
[general]
respond_by_dm_only = True
defaultChannel = 0
```
The weather forecasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use.
```
[location]
enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
favoriteNodeList = # list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
```
Modules can be disabled or enabled.
```
[bbs]
enabled = False
### MQTT Notes
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. Tested working fully Firmware:2.6.11 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
[general]
DadJokes = False
StoreForward = False
```
Sentry Bot detects anyone coming close to the bot-node
```
# detect anyone close to the bot
SentryEnabled = True
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by seconds(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
```
The BBS has admin and block lists; see the [config.template](config.template)
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) 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. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
```
# repeater module
[repeater]
enabled = True
repeater_channels = [2, 3]
```
A module allowing a Hamlib compatible radio to connect to the bot, when functioning it will message the channel configured with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```
[radioMon]
enabled = False
rigControlServerAddress = localhost:4532
# channel to broadcast to can be 2,3
sigWatchBroadcastCh = 2
# minimum SNR as reported by radio via hamlib
signalDetectionThreshold = -10
# hold time for high SNR
signalHoldTime = 10
# the following are combined to reset the monitor
signalCooldown = 5
signalCycleLimit = 5
```
Ollama Settings, for Ollama to work the command line `ollama run 'model'` needs to work properly. Check that you have enough RAM and your GPU are working as expected. The default model for this project, is set to `gemma2:2b` (run `ollama pull gemma2:2b` on command line, to download and setup)
- From the command terminal of your system with mesh-bot, download the default model for mesh-bot which is currently `ollama pull gemma2:2b`
Enable History, set via code readme Ollama Config in [Settings](https://github.com/SpudGunMan/meshing-around?tab=readme-ov-file#configurations) and [llm.py](https://github.com/SpudGunMan/meshing-around/blob/eb3bbdd3c5e0f16fe3c465bea30c781bd132d2d3/modules/llm.py#L12)
Tested models are `llama3.1, gemma2 (and variants), phi3.5, mistrial` other models may not handle the template as well.
```
# Enable ollama LLM see more at https://ollama.com
ollama = True
# Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma2
#ollamaModel = llama3.1
```
also see llm.py for changing the defaults of
```
# LLM System Variables
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
```
Logging messages to disk or Syslog to disk uses the python native logging function. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level.
```
[general]
# logging to file of the non Bot messages
LogMessagesToFile = True
# Logging of system messages to file
SyslogToFile = True
```
Example to log to disk only INFO and higher (ignore DEBUG)
```
*log.py
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
```
The Scheduler is enabled in the [settings.py](modules/settings.py) by setting `scheduler_enabled = True` the actions and settings are via code only at this time. see [mesh_bot.py](mesh_bot.py) around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit schedule its most flexible to edit raw code right now. See https://schedule.readthedocs.io/en/stable/ for more.
```
# 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))
```
# requirements
Python 3.10 minimally is needed, developed on latest release.
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
```
pip install meshtastic
pip install pubsub
```
mesh-bot enhancements
```
pip install pyephem
pip install requests
pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install geopy
pip install schedule
pip install wikipedia
```
The following is needed for open-meteo use
```
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
The following is for the Ollama LLM
```
pip install langchain
pip install langchain-ollama
pip install ollama
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
~~There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two~~
# Recognition
I used ideas and snippets from other responder bots and want to call them out!
- https://github.com/Murturtle/MeshLink
- https://github.com/pdxlocations/meshtastic-Python-Examples
- https://github.com/geoffwhittington/meshtastic-matrix-relay
Games Ported from..
- https://github.com/tigerpointe/Lemonade-Stand/
- https://github.com/Reconfirefly/drugwars
- https://github.com/Himan10/BlackJack
- https://github.com/devtronvarma/Video-Poker-Terminal-Game
### Inspiration and Code Snippets
- [MeshLink](https://github.com/Murturtle/MeshLink)
- [Meshtastic Python Examples](https://github.com/pdxlocations/meshtastic-Python-Examples)
- [Meshtastic Matrix Relay](https://github.com/geoffwhittington/meshtastic-matrix-relay)
GitHub user Nestpebble, for new ideas and enhancments, mrpatrick1991 For Docker configs, PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
Discord and Mesh user Cisien, and github Hailo1999, for testing and ideas! Lots of individuals on the Meshtastic discord who have tossed out ideas and tested code!
### Games Ported From
- [Lemonade Stand](https://github.com/tigerpointe/Lemonade-Stand/)
- [Drug Wars](https://github.com/Reconfirefly/drugwars)
- [BlackJack](https://github.com/Himan10/BlackJack)
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
- [Golf](https://github.com/danfriedman30/pythongame)
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
### Special Thanks
For testing and feature ideas on Discord and GitHub, if its stable its thanks to you all.
- **PiDiBi, Cisien, bitflip, nagu, Nestpebble, NomDeTom, Iris, Josh, GlockTuber, FJRPiolt, dj505, Woof, propstg, snydermesh, trs2982, F0X, Malice, mesb1, Hailo1999**
- **xdep**: For the reporting html. 📊
- **mrpatrick1991**: For OG Docker configurations. 💻
- **A-c0rN**: Assistance with iPAWS and 🚨
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **WH6GXZ nurse dude**: Volcano Alerts 🌋
- **mikecarper**: hamtest, leading to quiz etc.. 📋
- **c.merphy360**: high altitude alerts. 🚀
- **G7KSE**: DX Spotting idea. 📻
- **Growing List of GitHub Contributers**
- **Meshtastic Discord Community**: For putting up with 🥔
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
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.

71
compose.yaml Normal file
View File

@@ -0,0 +1,71 @@
services:
meshing-around:
ports:
- 8420:8420
#devices:
#- /dev/ttyUSB0:/dev/tty #update your config.ini to /dev/tty
#- /dev/ttyACM0:/dev/tty #if using serial select proper port
volumes:
- .:/app:rw
image: ghcr.io/spudgunman/meshing-around:main
container_name: meshing-around
restart: unless-stopped
environment:
- OLLAMA_API_URL=http://ollama:11434
extra_hosts:
- "host.docker.internal:host-gateway"
#user: "1000:1000"
#user: "10999:10999"
networks:
- meshing-around-network
test-bot:
image: ghcr.io/spudgunman/meshing-around:main
container_name: test-bot
command: ["/bin/bash", "-c", "python3 modules/test_bot.py | tee /tmp/test_tmp.txt; if grep -E 'failures=|errors=' /tmp/test_tmp.txt; then cp /tmp/test_tmp.txt /app/test_results.txt; fi"]
volumes:
- .:/app:rw
networks:
- meshing-around-network
stdin_open: true
debug-console:
image: ghcr.io/spudgunman/meshing-around:main
container_name: debug-console
command: ["/bin/bash"]
stdin_open: true
tty: true
volumes:
- .:/app:rw
networks:
- meshing-around-network
meshtasticd:
ports:
- 4403:4403
- 443:443
volumes:
- ./script/docker:/etc/meshtasticd:rw
restart: unless-stopped
container_name: meshtasticd
image: meshtastic/meshtasticd:daily-debian
networks:
- meshing-around-network
ollama:
ports:
- 11434:11434
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
networks:
- meshing-around-network
networks:
meshing-around-network:
external: true

View File

@@ -1,77 +1,186 @@
#config.ini
# type can be serial, tcp, or ble
# port is the serial port to use, commented out will try to auto-detect
# hostname is the IP address of the device to connect to for tcp type
# hostname is the IP/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for ble type
[interface]
type = serial
port = /dev/ttyACM0
# port = /dev/ttyUSB0
# port = COM1
# hostname = 192.168.0.1
# hostname = meshtastic.local
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support
# Additional interface for multi radio support
[interface2]
enabled = False
type = serial
port = /dev/ttyUSB0
#port = /dev/ttyACM1
# port = /dev/ttyACM1
# port = COM1
# hostname = meshtastic.local
# hostname = localhost
# mac = 00:11:22:33:44:55
# example, the third interface would be [interface3] up to 9
[general]
# if False will respond on all channels but the default channel
respond_by_dm_only = True
# defaultChannel is the meshtastic default public channel
# Allows auto-ping feature in a channel, False forces to 1 ping only
autoPingInChannel = False
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
defaultChannel = 0
# ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreDefaultChannel = False
# ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
ignoreChannels =
# require ! to be the first character in a command
cmdBang = False
# require explicit command, the message will only be processed if it starts with a command word
explicitCmd = True
# list of favorite nodes numbers ex: 2813308004,4258675309 used by script/addFav.py
favoriteNodeList =
# motd is reset to this value on boot
motd = Thanks for using MeshBOT! Have a good day!
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
# whoami
whoami = True
# enable or disable the Joke module
DadJokes = True
DadJokesEmoji = False
# enable or disable the Solar module
spaceWeather = True
# enable or disable the RSS module, and truncate the story
rssEnable = True
rssFeedURL = http://www.hackaday.com/rss.xml,http://rss.slashdot.org/Slashdot/slashdotMain,http://www.reddit.com/r/meshtastic/.rss
# RSS feed names must match the order of the URLs above, default is used if no match
rssFeedNames = default,slashdot,mesh
rssMaxItems = 3
rssTruncate = 100
# enable or disable the headline command which uses NewsAPI.org key at https://newsapi.org/register
enableNewsAPI = False
newsAPI_KEY =
newsAPIregion = us
# could also be 'relevancy' or 'popularity' or 'publishedAt'
sort_by = relevancy
# enable or disable the wikipedia search module
wikipedia = True
# Use local Kiwix server instead of online Wikipedia
# Set to False to use online Wikipedia, or provide Kiwix server URL
useKiwixServer = False
# Kiwix server URL (e.g., http://127.0.0.1:8080)
kiwixURL = http://127.0.0.1:8080
# Kiwix library name (e.g., wikipedia_en_100_nopic_2025-09)
kiwixLibraryName = wikipedia_en_100_nopic_2025-09
# Enable ollama LLM see more at https://ollama.com
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# Ollama model to use (defaults to gemma3:270m) gemma2 is good for older SYSTEM prompt
# ollamaModel = gemma3:latest
# ollamaModel = gemma2:2b
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# Produce LLM replies to messages that aren't commands?
# If False, the LLM only replies to the "ask:" and "askai" commands.
llmReplyToNonCommands = True
# if True, the input is sent raw to the LLM, if False uses SYSTEM prompt
rawLLMQuery = True
# Enable Wikipedia/Kiwix integration with LLM for RAG (Retrieval Augmented Generation)
# When enabled, LLM will automatically search Wikipedia/Kiwix and include context in responses
llmUseWikiContext = False
# Use OpenWebUI instead of direct Ollama API (enables advanced RAG features)
useOpenWebUI = False
# OpenWebUI server URL (e.g., http://localhost:3000)
openWebUIURL = http://localhost:3000
# OpenWebUI API key/token (required when useOpenWebUI is True)
openWebUIAPIKey =
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
reverseSF = False
# history command
enableCmdHistory = True
# command history ignore list ex: 2813308004,4258675309
lheardCmdIgnoreNodes =
# 24 hour clock
zuluTime = False
# wait time for URL requests
urlTimeout = 10
urlTimeout = 15
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = False
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
[games]
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
#Do not retry enabling interface if it fails, just exit to let OS restart the bot
dont_retry_disconnect = False
#echo command, will echo back your message as the bot
enableEcho = False
# command will only echo 1:1 if sent on this channel, otherwise it will prepend @yourname
echoChannel = 9
[emergencyHandler]
# enable or disable the emergency response handler
enabled = False
# channel to send a message to when the emergency handler is triggered
alert_channel = 2
alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
# device interface and channel to send the alert message to
SentryInterface = 1
SentryChannel = 2
emailSentryAlerts = False
# Enable detection sensor alert, requires external GPIO sensor connected to node
detectionSensorAlert = False
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# list of watched nodes numbers ex: 2813308004,4258675309
sentryWatchList =
# radius in meters to detect someone close to the bot
SentryRadius = 100
# channel to send a message to when the watchdog is triggered
SentryChannel = 9
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# 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
# Altitude in meters to trigger the alert
highFlyingAlertAltitude = 2000
# check with OpenSkyNetwork if highfly detected for aircraft
highflyOpenskynetwork = True
# Channel to send Alert when the high flying node is detected
highFlyingAlertInterface = 1
# to disable OTA alert set to unused channel like 9
highFlyingAlertChannel = 2
# list of nodes numbers to ignore high flying alert ex: 2813308004,4258675309
highFlyingIgnoreList =
[bbs]
enabled = True
@@ -79,21 +188,115 @@ enabled = True
bbs_ban_list =
# list of admin nodes numbers ex: 2813308004,4258675309
bbs_admin_list =
# enable bbs synchronization with other nodes
bbslink_enabled = False
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
bbslink_whitelist =
# enable API script access (increases disk i/o)
bbsAPI_enabled = False
# location module
[location]
enabled = True
lat = 48.50
lon = -123.0
# NOAA weather forecast days, the first two rows are today and tonight
NOAAforecastDuration = 4
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
fuzzConfigLocation = True
fuzzItAll = False
# Default to metric units rather than imperial
useMetric = False
# repeaterList lookup location (rbook / artsci / False)
repeaterLookup = rbook
# Satalite Pass Prediction
# 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
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# NOAA weather forecast days
NOAAforecastDuration = 3
# number of weather alerts to display
NOAAalertCount = 2
# NOAA Weather EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# Enable Ignore any message that includes following word list
ignoreEASenable = False
ignoreEASwords = test,advisory
# Add extra location to the weather alert
enableExtraLocationWx = False
# NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
coastalEnabled = False
# Find the correct costal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
# pz = Puget Sound, ph = Honolulu HI, gm = Florida Keys, pk = Alaska
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
# myCoastalZone is the .txt file with the forecast data
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt
# number of data points to return, default is 3
coastalForecastDays = 3
# NOAA USGS Hydrology river identifiers, LID or USGS ID https://waterdata.usgs.gov 12484500 Columbia River at The Dalles, OR
# for multiple rivers use comma separated list e.g. 12484500,14105700
riverList =
# USA FEMA IPAWS alerts
ipawsAlertEnabled = True
# 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
# Enable Ignore, headline that includes following word list
ignoreFEMAenable = True
ignoreFEMAwords = test,exercise
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
# Enable Ignore any message that includes following word list
ignoreUSGSEnable = False
ignoreUSGSWords = test,advisory
# Use Germany/DE Alert Broadcast Data
enableDEalerts = False
# comma separated list of regional codes trigger local alert.
# find your regional codet at https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json
myRegionalKeysDE = 110000000000,120510000000
# Alerts are sent to the emergency_handler interface and channel duplicate messages are send here if set
eAlertBroadcastCh =
# CheckList Checkin/Checkout
[checklist]
enabled = False
checklist_db = data/checklist.db
reverse_in_out = False
# Auto approve new checklists
auto_approve = True
# Check-in reminder interval is 5min
# Checkin broadcast interface and channel is emergency_handler interface and channel
# Inventory and Point of Sale System
[inventory]
enabled = False
inventory_db = data/inventory.db
# Set to True to disable penny precision and round to nickels (USA cash sales)
# When True: cash sales round down, taxed sales round up to nearest $0.05
# When False (default): normal penny precision ($0.01)
disable_penny = False
[qrz]
# QRZ Hello to new nodes with message
enabled = False
qrz_db = data/qrz.db
qrz_hello_string = "MeshBot says Hello! DM for more info."
# Training mode will not send the hello message to new nodes
training = True
# repeater module
[repeater]
enabled = False
@@ -101,14 +304,41 @@ enabled = False
# and rebroadcasted on the same channel on the other device/node/interface
# with great power comes great responsibility, danger could be lurking in use of this feature
# if you have the two nodes on the same radio configurations, you could create a feedback loop
repeater_channels =
repeater_channels =
[scheduler]
# enable or disable the scheduler module
enabled = False
# interface to send the message to
interface = 1
# channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
# enable overides the above and uses the motd as the message
schedulerMotd = False
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
# value can also be 'joke' (min/interval), 'weather' (time/day), 'link' (hour/interval) for special auto messages
# or 'news' (hour/interval), 'readrss' (hour/interval), 'mwx' (time/day), 'sysinfo' (hour/interval),
# 'tide' (time/day), 'solar' (time/day) for automated information broadcasts, matching module needs enabled!
# 'custom' for module/scheduler.py custom schedule examples
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
# time of day in 24:00 hour format when value is 'day' and interval is not set
time =
[radioMon]
# dx cluster `dx` command
dxspotter_enabled = True
# alerts in this module use the following interface and channel
sigWatchBroadcastInterface = 1
# broadcast channel can also be a comma separated list of channels
sigWatchBroadcastCh = 2
# using Hamlib rig control will monitor and alert on channel use
enabled = False
rigControlServerAddress = localhost:4532
# broadcast to all nodes on the channel can also be = 2,3
sigWatchBroadcastCh = 2
rigControlServerAddress = 127.0.0.1:4532
# minimum SNR as reported by radio via hamlib
signalDetectionThreshold = -10
# hold time for high SNR
@@ -117,10 +347,137 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
# Enable VOX detection using default input
voxDetectionEnabled = False
# description to use in the alert message
voxDescription = VOX
useLocalVoxModel = False
# default language for VOX detection
voxLanguage = en-us
# sound.card input device to use for VOX detection, 'default' uses system default
voxInputDevice = default
# "hey chirpy"
voxOnTrapList = True
voxTrapList = chirpy
# allow use of 'weather' and 'joke' commands via VOX
voxEnableCmd = True
# Meshages Text-to-Speech (TTS) for incoming messages and DM
meshagesTTS = False
ttsChannels = 2
# WSJT-X UDP monitoring - listens for decode messages from WSJT-X, FT8/FT4/WSPR etc.
wsjtxDetectionEnabled = False
# UDP address and port where WSJT-X broadcasts (default: 127.0.0.1:2237)
wsjtxUdpServerAddress = 127.0.0.1:2237
# Comma-separated list of callsigns to watch (empty = all callsigns)
wsjtxWatchedCallsigns =
# JS8Call TCP monitoring - connects to JS8Call API for message forwarding
js8callDetectionEnabled = False
# TCP address and port where JS8Call API listens (default: 127.0.0.1:2442)
js8callServerAddress = 127.0.0.1:2442
# Comma-separated list of callsigns to watch (empty = all callsigns)
js8callWatchedCallsigns =
[fileMon]
filemon_enabled = False
# text file to monitor for changes
file_path = alert.txt
# channel to send the message to can be 2,3 multiple channels comma separated
broadcastCh = 2
# news command will return the contents of a text file
enable_read_news = False
news_file_path = ../data/news.txt
# only return a single random line from the news file
news_random_line = False
# enable the use of exernal shell commands, this enables some data in `sysinfo`
enable_runShellCmd = False
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs
allowXcmd = False
# Enable 2 factor authentication for x: commands
2factor_enabled = True
# time in seconds to wait for the correct 2FA answer
2factor_timeout = 100
[smtp]
# enable or disable the SMTP module
enableSMTP = False
# enable or disable the IMAP module for inbound email
enableImap = False
# list of Sysop Emails seperate with commas
sysopEmails =
SMTP_SERVER = smtp.gmail.com
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
SMTP_PORT = 587
# Sender email: be mindful of public access, don't use your personal email
FROM_EMAIL = none@gmail.com
SMTP_AUTH = True
SMTP_USERNAME = none@gmail.com
SMTP_PASSWORD = none
EMAIL_SUBJECT = Meshtastic✉
# IMAP not implimented yet
IMAP_SERVER = imap.gmail.com
# 993 IMAP over TLS/SSL, 143 legacy IMAP
IMAP_PORT = 993
# IMAP login usually same as SMTP
IMAP_USERNAME = none@gmail.com
IMAP_PASSWORD = none
IMAP_FOLDER = inbox
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
disable_emojis = False
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
mastermind = True
golfsim = True
hangman = True
hamtest = True
tictactoe = True
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
survey = False
# this is the default survey to use when command givcen, from data/survey/example_survey.json
defaultSurvey = example
# Whether to record user ID in responses
surveyRecordID=True
# Whether to record location on start of survey
surveyRecordLocation=True
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7
# delay in seconds for splits in messages to avoid message collision
splitDelay = 0.7
# message chunk size for sending at high success rate
# delay in seconds for response to avoid message collision /throttling
responseDelay = 2.2
# delay in seconds for splits in messages to avoid message collision /throttling
splitDelay = 2.5
# message chunk size in charcters, chunkr allows exceeding by 3 characters
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing in bytes
maxBuffer = 200
#Enable Extra logging of Hop count data
enableHopLogs = False
# Noisy Node Telemetry Logging and packet threshold
noisyNodeLogging = False
noisyTelemetryLimit = 5
logMetaStats = True
# Enable detailed packet logging all packets
DEBUGpacket = False
# metaPacket detailed logging, the filter negates the port ID
debugMetadata = False
metadataFilter = TELEMETRY_APP,POSITION_APP

1
data/README.md Normal file
View File

@@ -0,0 +1 @@
database admin tool is in [./etc/db_admin.py](../etc/db_admin.py)

7226
data/hamradio/extra.json Normal file

File diff suppressed because it is too large Load Diff

5126
data/hamradio/general.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
data/mesh_news.txt Normal file
View File

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

1
data/news.txt Normal file
View File

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

16
data/quiz_questions.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,264 @@
# Implementation Summary: Enhanced Check-in/Check-out and Point of Sale System
## Overview
This implementation addresses the GitHub issue requesting enhancements to the check-in/check-out system and the addition of a complete Point of Sale (POS) functionality to the meshing-around project.
## What Was Implemented
### 1. Enhanced Check-in/Check-out System
#### New Features Added:
- **Time Window Monitoring**: Check-in with safety intervals (e.g., `checkin 60 Hunting in tree stand`)
- Tracks if users don't check in within expected timeframe
- Ideal for solo activities, remote work, or safety accountability
- Provides `get_overdue_checkins()` function for alert integration
- **Approval Workflow**:
- `checklistapprove <id>` - Approve pending check-ins (admin)
- `checklistdeny <id>` - Deny/remove check-ins (admin)
- Support for approval-based workflows
- **Enhanced Database Schema**:
- Added `approved` field for approval workflows
- Added `expected_checkin_interval` field for safety monitoring
- Automatic migration for existing databases
#### New Commands:
- `checklistapprove <id>` - Approve a check-in
- `checklistdeny <id>` - Deny a check-in
- Enhanced `checkin [interval] [note]` - Now supports interval parameter
### 2. Complete Point of Sale System
#### Features Implemented:
**Item Management:**
- Add items with price, quantity, and location
- Remove items from inventory
- Update item prices and quantities
- Quick sell functionality
- Transaction returns/reversals
- Full inventory listing with valuations
**Cart System:**
- Per-user shopping carts
- Add/remove items from cart
- View cart with totals
- Complete transactions (buy/sell)
- Clear cart functionality
**Financial Features:**
- Penny rounding support (USA mode)
- Cash sales round down to nearest nickel
- Taxed sales round up to nearest nickel
- Transaction logging with full audit trail
- Daily sales statistics
- Revenue tracking
- Hot item detection (best sellers)
**Database Schema:**
Four tables for complete functionality:
- `items` - Product inventory
- `transactions` - Sales records
- `transaction_items` - Line items per transaction
- `carts` - Temporary shopping carts
#### Commands Implemented:
**Item Management:**
- `itemadd <name> <price> <qty> [location]` - Add new item
- `itemremove <name>` - Remove item
- `itemreset <name> [price=X] [qty=Y]` - Update item
- `itemsell <name> <qty> [notes]` - Quick sale
- `itemreturn <transaction_id>` - Reverse transaction
- `itemlist` - View all inventory
- `itemstats` - Daily statistics
**Cart System:**
- `cartadd <name> <qty>` - Add to cart
- `cartremove <name>` - Remove from cart
- `cartlist` / `cart` - View cart
- `cartbuy` / `cartsell [notes]` - Complete transaction
- `cartclear` - Empty cart
## Files Created/Modified
### New Files:
1. **modules/inventory.py** (625 lines)
- Complete inventory and POS module
- All item management functions
- Cart system implementation
- Transaction processing
- Penny rounding logic
2. **modules/inventory.md** (8,529 chars)
- Comprehensive user guide
- Command reference
- Use case examples
- Database schema documentation
3. **modules/checklist.md** (9,058 chars)
- Enhanced checklist user guide
- Safety monitoring documentation
- Best practices
- Scenario examples
### Modified Files:
1. **modules/checklist.py**
- Added time interval monitoring
- Added approval workflow functions
- Enhanced database schema
- Updated command processing
2. **modules/settings.py**
- Added inventory configuration section
- Added `inventory_enabled` setting
- Added `inventory_db` path setting
- Added `disable_penny` setting
3. **config.template**
- Added `[inventory]` section
- Documentation for penny rounding
4. **modules/system.py**
- Integrated inventory module
- Added trap list for inventory commands
5. **mesh_bot.py**
- Added inventory command handlers
- Added checklist approval commands
- Created `handle_inventory()` function
6. **modules/README.md**
- Updated checklist section with new features
- Added complete inventory/POS section
- Updated table of contents
7. **.gitignore**
- Added database files to ignore list
## Configuration
### Enable Inventory System:
```ini
[inventory]
enabled = True
inventory_db = data/inventory.db
disable_penny = False # Set to True for USA penny rounding
```
### Checklist Already Configured:
```ini
[checklist]
enabled = False # Set to True to enable
checklist_db = data/checklist.db
reverse_in_out = False
```
## Testing Results
All functionality tested and verified:
- ✅ Module imports work correctly
- ✅ Database initialization successful
- ✅ Inventory commands function properly
- ✅ Cart system working as expected
- ✅ Checklist enhancements operational
- ✅ Time interval monitoring active
- ✅ Trap lists properly registered
- ✅ Help commands return correct information
## Use Cases Addressed
### From Issue Comments:
1. **Point of Sale Logic**
- Complete POS system with inventory management
- Cart-based transactions
- Sales tracking and reporting
2. **Check-in Time Windows**
- Interval-based monitoring
- Overdue detection
- Safety accountability for solo activities
3. **Geo-location Awareness**
- Automatic GPS capture when checking in/out
- Location stored with each check-in
- Foundation for "are you ok" alerts
4. **Asset Management**
- Track any type of asset (tools, equipment, supplies)
- Multiple locations support
- Full transaction history
5. **Penny Rounding**
- Configurable USA cash sale rounding
- Separate logic for cash vs taxed sales
- Down for cash, up for tax
## Security Features
- Users on `bbs_ban_list` cannot use inventory or checklist commands
- Admin-only approval commands
- Parameterized SQL queries prevent injection
- Per-user cart isolation
- Full transaction audit trail
## Documentation Provided
1. **User Guides:**
- Comprehensive inventory.md with examples
- Detailed checklist.md with safety scenarios
- Updated main README.md
2. **Technical Documentation:**
- Database schema details
- Configuration examples
- Command reference
- API documentation in code comments
3. **Examples:**
- Emergency supply tracking
- Event merchandise sales
- Field equipment management
- Safety monitoring scenarios
## Future Enhancement Opportunities
The implementation provides foundation for:
- Scheduled overdue check-in alerts
- Email/SMS notifications for overdue status
- Dashboard/reporting interface
- Barcode/QR code support
- Multi-location inventory tracking
- Inventory forecasting
- Integration with external systems
## Backward Compatibility
- Existing checklist databases automatically migrate
- New features are opt-in via configuration
- No breaking changes to existing commands
- Graceful handling of missing database columns
## Performance Considerations
- SQLite databases for reliability and simplicity
- Indexed primary keys for fast lookups
- Efficient query design
- Minimal memory footprint
- No external dependencies beyond stdlib
## Conclusion
This implementation fully addresses all requirements from the GitHub issue:
- ✅ Enhanced check-in/check-out with SQL improvements
- ✅ Point of sale logic with inventory management
- ✅ Time window notifications for safety
- ✅ Asset tracking for any item type
- ✅ Penny rounding for USA cash sales
- ✅ Cart management system
- ✅ Comprehensive documentation
The system is production-ready, well-tested, and documented for immediate use.

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.

View File

@@ -1,54 +0,0 @@
# Load the bbs messages from the database file to screen for admin functions
import pickle # pip install pickle
# load the bbs messages from the database file
try:
with open('../bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
try:
with open('bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
print ("\nSystem: bbsdb.pkl not found")
try:
with open('../bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
try:
with open('bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
print ("\nSystem: bbsdm.pkl not found")
# Game HS tables
try:
with open('../lemonade_hs.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except:
try:
with open('lemonade_hs.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except:
print ("\nSystem: lemonade_hs.pkl not found")
try:
with open('../dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except:
try:
with open('dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except:
print ("\nSystem: dopewar_hs.pkl not found")
print ("\nSystem: bbs_messages")
print (bbs_messages)
print ("\nSystem: bbs_dm")
print (bbs_dm)
print ("Game HS tables")
print (f"lemon:{lemon_score}")
print (f"dopewar:{dopewar_score}")

View File

@@ -0,0 +1,125 @@
#!/usr/bin/python3
import schedule
from modules.log import logger
from modules.settings import MOTD
from modules.system import send_message
def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc, MOTD, schedulerChannel, schedulerInterface):
"""
Set up custom schedules. Edit the example schedules as needed.
1. in config.ini set "value" under [scheduler] to: value = custom
2. edit this file to add/remove/modify schedules
3. restart mesh bot
4. verify schedules are working by checking the log file
5. Make sure to uncomment (delete the single #) the example schedules down at the end of the file to enable them
Python is sensitive to indentation so be careful when editing this file.
https://thonny.org is included on pi's image and is a simple IDE to use for editing python files.
Available functions you can import and use, be sure they are enabled modules in config.ini:
- tell_joke() - Returns a random joke
- welcome_message - A welcome message string
- handle_wxc(message_from_id, deviceID, cmd, days=None) - Weather information
- handleNews(message_from_id, deviceID, message, isDM) - News reader
- get_rss_feed(msg) - RSS feed reader
- handle_mwx(message_from_id, deviceID, cmd) - Marine weather
- sysinfo(message, message_from_id, deviceID, isDM) - System information
- handle_tide(message_from_id, deviceID, channel_number) - Tide information
- handle_sun(message_from_id, deviceID, channel_number) - Sun information
- MOTD - Message of the day string
"""
try:
# Import additional functions for scheduling (optional, depending on your needs)
from mesh_bot import handleNews, sysinfo, handle_mwx, handle_tide, handle_sun
from modules.rss import get_rss_feed
# Example task functions, modify as needed the channel and interface parameters default to schedulerChannel and schedulerInterface
def send_joke(channel, interface):
## uses system.send_message to send the result of tell_joke()
send_message(tell_joke(), channel, 0, interface)
def send_good_morning(channel, interface):
## uses system.send_message to send "Good Morning"
send_message("Good Morning", channel, 0, interface)
def send_wx(channel, interface):
## uses system.send_message to send the result of handle_wxc(id,id,cmd,days_returned)
send_message(handle_wxc(0, 1, 'wx', days=1), channel, 0, interface)
def send_weather_alert(channel, interface):
## uses system.send_message to send string
send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", channel, 0, interface)
def send_config_url(channel, interface):
## uses system.send_message to send string
send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", channel, 0, interface)
def send_net_starting(channel, interface):
## uses system.send_message to send string, channel 2, interface 3
send_message("Net Starting Now", 2, 0, 3)
def send_welcome(channel, interface):
## uses system.send_message to send string, channel 2, interface 1
send_message("Welcome to the group", 2, 0, 1)
def send_motd(channel, interface):
## uses system.send_message to send message of the day string which can be updated in runtime
send_message(MOTD, channel, 0, interface)
def send_news(channel, interface):
## uses system.send_message to send the result of handleNews()
send_message(handleNews(0, interface, 'readnews', False), channel, 0, interface)
def send_rss(channel, interface):
## uses system.send_message to send the result of get_rss_feed()
send_message(get_rss_feed(''), channel, 0, interface)
def send_marine_weather(channel, interface):
## uses system.send_message to send the result of handle_mwx()
send_message(handle_mwx(0, interface, 'mwx'), channel, 0, interface)
def send_sysinfo(channel, interface):
## uses system.send_message to send the result of sysinfo()
send_message(sysinfo('', 0, interface, False), channel, 0, interface)
def send_tide(channel, interface):
## uses system.send_message to send the result of handle_tide()
send_message(handle_tide(0, interface, channel), channel, 0, interface)
def send_sun(channel, interface):
## uses system.send_message to send the result of handle_sun()
send_message(handle_sun(0, interface, channel), channel, 0, interface)
### Send a joke every 2 minutes
#schedule.every(2).minutes.do(lambda: send_joke(schedulerChannel, schedulerInterface))
### Send a good morning message every day at 9 AM
#schedule.every().day.at("09:00").do(lambda: send_good_morning(schedulerChannel, schedulerInterface))
### Send weather update every day at 8 AM
#schedule.every().day.at("08:00").do(lambda: send_wx(schedulerChannel, schedulerInterface))
### Send weather alerts every Wednesday at noon
#schedule.every().wednesday.at("12:00").do(lambda: send_weather_alert(schedulerChannel, schedulerInterface))
### Send configuration URL every 2 days at 10 AM
#schedule.every(2).days.at("10:00").do(lambda: send_config_url(schedulerChannel, schedulerInterface))
### Send net starting message every Wednesday at 7 PM
#schedule.every().wednesday.at("19:00").do(lambda: send_net_starting(schedulerChannel, schedulerInterface))
### Send welcome message every 2 days at 8 AM
#schedule.every(2).days.at("08:00").do(lambda: send_welcome(schedulerChannel, schedulerInterface))
### Send MOTD every day at 1 PM
#schedule.every().day.at("13:00").do(lambda: send_motd(schedulerChannel, schedulerInterface))
### Send bbslink message every 2 days at 10 AM
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
### Send news updates every 6 hours
#schedule.every(6).hours.do(lambda: send_news(schedulerChannel, schedulerInterface))
### Send RSS feed every day at 9 AM
#schedule.every().day.at("09:00").do(lambda: send_rss(schedulerChannel, schedulerInterface))
### Send marine weather every day at 6 AM
#schedule.every().day.at("06:00").do(lambda: send_marine_weather(schedulerChannel, schedulerInterface))
### Send system information every day at 12 PM
#schedule.every().day.at("12:00").do(lambda: send_sysinfo(schedulerChannel, schedulerInterface))
### Send tide information every day at 5 AM
#schedule.every().day.at("05:00").do(lambda: send_tide(schedulerChannel, schedulerInterface))
### Send sun information every day at 6 AM
#schedule.every().day.at("06:00").do(lambda: send_sun(schedulerChannel, schedulerInterface))
except Exception as e:
logger.error(f"Error setting up custom schedules: {e}")

191
etc/db_admin.py Normal file
View File

@@ -0,0 +1,191 @@
# Load the bbs messages from the database file to screen for admin functions
import pickle
import sqlite3
print ("\n Meshing-Around Database Admin Tool\n")
# load the bbs messages from the database file
try:
with open('../data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
try:
with open('data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
bbs_messages = "System: data/bbsdb.pkl not found"
try:
with open('../data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except Exception as e:
try:
with open('data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except Exception as e:
bbs_dm = "System: data/bbsdm.pkl not found"
try:
with open('../data/email_db.pickle', 'rb') as f:
email_db = pickle.load(f)
except Exception as e:
try:
with open('data/email_db.pickle', 'rb') as f:
email_db = pickle.load(f)
except Exception as e:
email_db = "System: data/email_db.pickle not found"
try:
with open('../data/sms_db.pickle', 'rb') as f:
sms_db = pickle.load(f)
except Exception as e:
try:
with open('data/sms_db.pickle', 'rb') as f:
sms_db = pickle.load(f)
except Exception as e:
sms_db = "System: data/sms_db.pickle not found"
# Game HS tables
try:
with open('../data/lemonstand.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except Exception as e:
try:
with open('data/lemonstand.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except Exception as e:
lemon_score = "System: data/lemonstand.pkl not found"
try:
with open('../data/dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except Exception as e:
try:
with open('data/dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except Exception as e:
dopewar_score = "System: data/dopewar_hs.pkl not found"
try:
with open('../data/blackjack_hs.pkl', 'rb') as f:
blackjack_score = pickle.load(f)
except Exception as e:
try:
with open('data/blackjack_hs.pkl', 'rb') as f:
blackjack_score = pickle.load(f)
except Exception as e:
blackjack_score = "System: data/blackjack_hs.pkl not found"
try:
with open('../data/videopoker_hs.pkl', 'rb') as f:
videopoker_score = pickle.load(f)
except Exception as e:
try:
with open('data/videopoker_hs.pkl', 'rb') as f:
videopoker_score = pickle.load(f)
except Exception as e:
videopoker_score = "System: data/videopoker_hs.pkl not found"
try:
with open('../mmind_hs.pkl', 'rb') as f:
mmind_score = pickle.load(f)
except Exception as e:
try:
with open('mmind_hs.pkl', 'rb') as f:
mmind_score = pickle.load(f)
except Exception as e:
mmind_score = "System: mmind_hs.pkl not found"
try:
with open('../data/golfsim_hs.pkl', 'rb') as f:
golfsim_score = pickle.load(f)
except Exception as e:
try:
with open('data/golfsim_hs.pkl', 'rb') as f:
golfsim_score = pickle.load(f)
except Exception as e:
golfsim_score = "System: data/golfsim_hs.pkl not found"
# checklist.db admin display
print("\nCurrent Check-ins Table\n")
try:
conn = sqlite3.connect('../data/checklist.db')
except Exception:
conn = sqlite3.connect('data/checklist.db')
c = conn.cursor()
try:
c.execute("""
SELECT * FROM checkin
WHERE removed = 0
ORDER BY checkin_id DESC
LIMIT 20
""")
rows = c.fetchall()
col_names = [desc[0] for desc in c.description]
if rows:
# Print header
header = " | ".join(f"{name:<15}" for name in col_names)
print(header)
print("-" * len(header))
# Print rows
for row in rows:
print(" | ".join(f"{str(col):<15}" for col in row))
else:
print("No check-ins found.")
except Exception as e:
print(f"Error reading check-ins: {e}")
finally:
conn.close()
# inventory.db admin display
print("\nCurrent Inventory Table\n")
try:
conn = sqlite3.connect('../data/inventory.db')
except Exception:
conn = sqlite3.connect('data/inventory.db')
c = conn.cursor()
try:
c.execute("""
SELECT * FROM inventory
ORDER BY item_id DESC
LIMIT 20
""")
rows = c.fetchall()
col_names = [desc[0] for desc in c.description]
if rows:
# Print header
header = " | ".join(f"{name:<15}" for name in col_names)
print(header)
print("-" * len(header))
# Print rows
for row in rows:
print(" | ".join(f"{str(col):<15}" for col in row))
else:
print("No inventory items found.")
except Exception as e:
print(f"Error reading inventory: {e}")
finally:
conn.close()
# Pickle database displays
print ("System: bbs_messages")
print (bbs_messages)
print ("\nSystem: bbs_dm")
print (bbs_dm)
print ("\nSystem: email_db")
print (email_db)
print ("\nSystem: sms_db")
print (sms_db)
print (f"\n\nGame HS tables\n")
print (f"lemon:{lemon_score}")
print (f"dopewar:{dopewar_score}")
print (f"blackjack:{blackjack_score}")
print (f"videopoker:{videopoker_score}")
print (f"mmind:{mmind_score}")
print (f"golfsim:{golfsim_score}")
print ("\n")

49
etc/eas_alert_parser.py Normal file
View File

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

102
etc/fakeNode.py Normal file
View File

@@ -0,0 +1,102 @@
# https://github.com/pdxlocations/mudp/blob/main/examples/helloworld-example.py
import time
import random
from pubsub import pub
from meshtastic.protobuf import mesh_pb2
from mudp import (
conn,
node,
UDPPacketStream,
send_nodeinfo,
send_text_message,
send_device_telemetry,
send_position,
send_environment_metrics,
send_power_metrics,
send_waypoint,
)
MCAST_GRP = "224.0.0.69"
MCAST_PORT = 4403
KEY = "1PG7OiApB1nwvP+rz05pAQ=="
interface = UDPPacketStream(MCAST_GRP, MCAST_PORT, key=KEY)
def setup_node():
node.node_id = "!deadbeef"
node.long_name = "UDP Test"
node.short_name = "UDP"
node.channel = "LongFast"
node.key = "AQ=="
conn.setup_multicast(MCAST_GRP, MCAST_PORT)
# Convert hex node_id to decimal (strip the '!' first)
decimal_id = int(node.node_id[1:], 16)
print(f"Node ID: {node.node_id} (decimal: {decimal_id})")
print(f"Channel: {node.channel}, Key: {node.key}")
def demo_send_messages():
print("Sending node info...")
send_nodeinfo()
time.sleep(3)
print("Sending text message...")
send_text_message("hello world")
time.sleep(3)
print("Sending device telemetry position...")
send_position(latitude=37.7749, longitude=-122.4194, altitude=3000, precision_bits=3, ground_speed=5)
time.sleep(3)
print("Sending device telemetry local node data...")
send_device_telemetry(battery_level=50, voltage=3.7, channel_utilization=25, air_util_tx=15, uptime_seconds=123456)
time.sleep(3)
print("Sending environment metrics...")
send_environment_metrics(
temperature=23.072298,
relative_humidity=17.5602016,
barometric_pressure=995.36261,
gas_resistance=229.093369,
voltage=5.816,
current=-29.3,
iaq=66,
)
time.sleep(3)
print("Sending power metrics...")
send_power_metrics(
ch1_voltage=18.744,
ch1_current=11.2,
ch2_voltage=2.792,
ch2_current=18.4,
ch3_voltage=0,
ch3_current=0,
)
time.sleep(3)
print("Sending waypoint...")
send_waypoint(
id=random.randint(1, 2**32 - 1),
latitude=45.271394,
longitude=-121.736083,
expire=0,
locked_to=node.node_id,
name="Camp",
description="Main campsite near the lake",
icon=0x1F3D5, # 🏕
)
def main():
setup_node()
interface.start()
print("MUDP Fake Node is running. Press Ctrl+C to exit.")
print("You can send demo messages to the network.")
try:
while True:
answer = input("Do you want to send demo messages? (y/n): ").strip().lower()
if answer == "y":
demo_send_messages()
elif answer == "n":
print("Exiting.")
break
except KeyboardInterrupt:
pass
finally:
interface.stop()
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/mesh_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot.service
# sudo systemctl start mesh_bot.service
[Unit]
@@ -7,15 +8,21 @@ Description=MESH-BOT
After=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/dir/
ExecStart=/usr/bin/bash /dir/launch.sh mesh
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
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,12 @@
[Unit]
Description=MeshingAround-ReportingTask
[Timer]
OnCalendar=*-*-* 04:20:00
Persistent=true
Unit=mesh_bot_reporting.service
#OnUnitActiveSec=1h
#OnbootSec=5min
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,28 @@
# /etc/systemd/system/mesh_bot_reporting.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot_reporting.service
# sudo systemctl start mesh_bot_reporting.service
[Unit]
Description=MeshingAround-Reporting
After=network.target
[Service]
Type=oneshot
User=pi
Group=pi
WorkingDirectory=/dir/
ExecStart=python3 etc/report_generator5.py
ExecStop=pkill -f report_generator5.py
# ExecStart=python3 etc/report_generator.py
# ExecStop=pkill -f report_generator.py
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,21 @@
[Unit]
Description=MeshingAround-WebServer
After=network.target
[Service]
Type=simple
User=pi
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
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable pong_bot.service
# sudo systemctl start pong_bot.service
[Unit]
@@ -7,15 +8,20 @@ Description=PONG-BOT
After=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/dir/
ExecStart=/usr/bin/bash /dir/launch.sh pong
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
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems
[Install]
WantedBy=default.target

1007
etc/report_generator.py Normal file

File diff suppressed because it is too large Load Diff

1302
etc/report_generator5.py Normal file

File diff suppressed because it is too large Load Diff

BIN
etc/reporting.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

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

40
etc/set-permissions.sh Normal file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Set ownership and permissions for Meshing Around application
# Check if run as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Use first argument as user, or default to meshbot
TARGET_USER="${1:-meshbot}"
# Check if user exists
if ! id "$TARGET_USER" &>/dev/null; then
echo "User '$TARGET_USER' does not exist."
read -p "Would you like to use the current user ($(logname)) instead? [y/N]: " yn
if [[ "$yn" =~ ^[Yy]$ ]]; then
TARGET_USER="$(logname)"
echo "Using current user: $TARGET_USER"
if ! id "$TARGET_USER" &>/dev/null; then
echo "Current user '$TARGET_USER' does not exist or cannot be determined."
exit 1
fi
else
echo "Exiting."
exit 1
fi
fi
echo "Setting ownership to $TARGET_USER:$TARGET_USER"
chown -R "$TARGET_USER:$TARGET_USER" "/opt/meshing-around/-around"
chown -R "$TARGET_USER:$TARGET_USER" "/opt/meshing-around/-around/logs"
chown -R "$TARGET_USER:$TARGET_USER" "/opt/meshing-around/-around/data"
chown "$TARGET_USER:$TARGET_USER" "/opt/meshing-around/-around/config.ini"
chmod 640 "/opt/meshing-around/-around/config.ini"
chmod 750 "/opt/meshing-around/-around/logs"
chmod 750 "/opt/meshing-around/-around/data"
echo "Permissions and ownership have been set."

View File

@@ -1,13 +1,16 @@
#!/usr/bin/env python3
# # Simulate meshing-around de K7MHI 2024
from modules.log import * # err? Move .py out of etc/ and place it in the root of the project
from modules.log import logger, getPrettyTime # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
import time
import datetime
import random
# Initialize the tool
projectName = "example_handler" # name of _handler function to match the function name under test
randomNode = False # Set to True to use random node IDs
# bot.py Simulated functions
deviceID = 1 # represents the device/node number
def get_NodeID():
nodeList = [4258675309, 1212121212, 1234567890, 9876543210]
if randomNode:
@@ -15,18 +18,45 @@ def get_NodeID():
else:
nodeID = nodeList[0]
return nodeID
nodeID = get_NodeID() # assign a nodeID
def get_name_from_number(nodeID, length='short', interface=1):
# return random name for nodeID
names = ["Max","Molly","Jake","Kelly"]
return names[nodeID % len(names)]
def mesh_bot(message, nodeID, deviceID):
return "Meshing-Around Bot at your service!"
#simulate GPS locations for testing
locations = [
(48.200909, -123.25719),
(48.330283,-123.260703),
(48.342735,-122.987911),
(48.205591,-122.998448)
]
lat, lon = random.choice(locations) # pick a random location
location = f"{lat},{lon}"
# # end Initialization of the tool
# # Function to handle, or the project in test
#from modules.llm import * # Import the LLM module
# # Project handler function code here
# example handler function canada()
def example_handler(message, nodeID, deviceID):
if message != "":
# put code in test here
msg = f"Hello {get_name_from_number(nodeID)}, simulator ready for testing {projectName} project! on device {deviceID}"
msg += f" Your location is {location}"
msg += f" you said: {message}"
return msg
def example_handler(nodeID, message):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "
msg += f" Its: {readableTime} "
msg += f" You just sent: {message}"
return msg
# # end of function test code
@@ -37,7 +67,7 @@ if __name__ == '__main__': # represents the bot's main loop
nodeInt = 1 # represents the device/node number
logger.info(f"System: Meshing-Around Simulator Starting for {projectName}")
nodeID = get_NodeID() # assign a nodeID
projectResponse = globals()[projectName](nodeID, " ") # Call the project handler under test
projectResponse = globals()[projectName]("", nodeID, deviceID) # call the handler function once to start
while True: # represents the onReceive() loop in the bot.py
projectResponse = ""
responseLength = 0
@@ -46,7 +76,7 @@ if __name__ == '__main__': # represents the bot's main loop
packet = input(f"CLIENT {nodeID} INPUT: " ) # Emulate the client input
if packet != "":
#try:
projectResponse = globals()[projectName](nodeID, packet) # Call the project handler under test
projectResponse = globals()[projectName](message = packet, nodeID = nodeID, deviceID = deviceID) # call the handler function
# except Exception as e:
# logger.error(f"System: Handler: {e}")
# projectResponse = "Error in handler"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

252
etc/www/localscripts/css2 Normal file
View File

@@ -0,0 +1,252 @@
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,640 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,24 +1,36 @@
#!/bin/bash
# This script launches the meshing-around bot or the report generator in python virtual environment
# launch.sh
cd "$(dirname "$0")"
# activate the virtual environment if it exists
if [ -d "venv" ]; then
source venv/bin/activate
fi
if [ ! -f "config.ini" ]; then
if [[ ! -f "config.ini" ]]; then
cp config.template config.ini
fi
# launch the application
if [ "$1" == "pong" ]; then
python3 pong_bot.py
elif [ "$1" == "mesh" ]; then
python3 mesh_bot.py
# activate the virtual environment if it exists
if [[ -d "venv" ]]; then
source venv/bin/activate
else
printf "\nPlease provide a bot to launch (pong/mesh)"
echo "Virtual environment not found, this tool just launches the .py in venv"
exit 1
fi
# launch the application
if [[ "$1" == pong* ]]; then
python3 pong_bot.py
elif [[ "$1" == mesh* ]]; then
python3 mesh_bot.py
elif [[ "$1" == "html" ]]; then
python3 etc/report_generator.py
elif [[ "$1" == "html5" ]]; then
python3 etc/report_generator5.py
elif [[ "$1" == add* ]]; then
python3 script/addFav.py
else
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5) or addFav"
exit 1
fi
deactivate

47
logs/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Logs and Reports
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)
## Settings
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
```conf
[general]
# logging to file of the non Bot messages only
LogMessagesToFile = False
# Logging of system messages to file, needed for reporting engine
SyslogToFile = True
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
## Web Reporting WebServer
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
find it at. http://localhost:8420
If you have linux-native running and errors such as..
```bash
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
socketserver.TCPServer.server_bind(self)
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
self.socket.bind(self.server_address)
```
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
```python
# Set the desired IP address
server_ip = '127.0.0.1'
```

File diff suppressed because it is too large Load Diff

1199
modules/README.md Normal file

File diff suppressed because it is too large Load Diff

207
modules/adding_more.md Normal file
View File

@@ -0,0 +1,207 @@
# 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.
---
## Overview Unit Tests
Your test_bot.py file contains a comprehensive suite of unit tests for the various modules the project. The tests are organized using Pythons `unittest` framework and cover both core utility modules and all major game modules.
---
## Structure
- **Imports & Setup:**
The script sets up the environment, imports all necessary modules, and suppresses certain warnings for clean test output.
- **TestBot Class:**
All tests are methods of the `TestBot` class, which inherits from `unittest.TestCase`.
---
## Core Module Tests
- **Database & Checklist:**
- `test_load_bbsdb`, `test_bbs_list_messages`, `test_initialize_checklist_database`
- **News & Alerts:**
- `test_init_news_sources`, `test_get_nina_alerts`
- **LLM & Wikipedia:**
- `test_llmTool_get_google`, `test_send_ollama_query`, `test_get_wikipedia_summary`, `test_get_kiwix_summary`
- **Space & Weather:**
- `test_get_moon_phase`, `test_get_sun_times`, `test_hf_band_conditions`
- **Radio & Location:**
- `test_get_hamlib`, `test_get_rss_feed`, `get_openskynetwork`, `test_initalize_qrz_database`
---
## Game Module Tests
Each game module has a dedicated test that simulates a typical user interaction:
- **Tic-Tac-Toe:**
Starts a game and makes one move.
- **Video Poker:**
Starts a session and places a bet.
- **Blackjack:**
Starts a game and places a bet.
- **Hangman:**
Starts a game and guesses a letter.
- **Lemonade Stand:**
Starts a game and buys a box of cups.
- **GolfSim:**
Starts a hole and takes a shot.
- **DopeWars:**
Starts a game, selects a city, and checks the list.
- **MasterMind:**
Starts a game and makes one guess.
- **Quiz:**
Starts a quiz, joins as a player, answers one question, and ends the quiz.
- **Survey:**
Starts a survey, answers one question, and ends the survey.
- **HamTest:**
Starts a ham radio test and answers one question.
---
## Extended API Tests
If the `.checkall` file is present, additional API and data-fetching tests are run for:
- RepeaterBook, ArtSciRepeaters, NOAA tides/weather, USGS earthquakes/volcanoes, satellite passes, and more.
## Notes
- Tests are designed to be **non-destructive** and **idempotent**.
- Some tests require specific data files (e.g., for quiz, survey, hamtest).
- The suite is intended to be run from the main program directory.
Happy hacking!

234
modules/bbstools.md Normal file
View File

@@ -0,0 +1,234 @@
---
# 📡 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)
- [Central Message Store](#11-central-message-store)
- [Direct Mail (DM) Messages](#12-direct-mail-dm-messages)
- [BBS Commands](#bbs-commands)
2. [Synchronization bot2bot: Full Sync Workflow](#2-synchronization-bot2bot--full-sync-workflow)
- [BBS Database Sync: File-Based (Out-of-Band)](#21-bbs-database-sync-file-based-out-of-band)
- [BBS Over-the-Air (OTA) Sync: Linking](#22-bbs-over-the-air-ota-sync-linking)
- [Scheduling BBS Auto Sync](#23-scheduling-bbs-auto-sync)
3. [Troubleshooting](#4-troubleshooting)
4. [API Reference: BBS Sync](#5-api-reference-bbs-sync)
5. [Best Practices](#5-best-practices)
## 1. **BBS Core Functions**
The mesh-bot provides a basic message mail system for Meshtastic
## 1.1 Central Message Store
- **Shared public message space** for all nodes.
- Classic BBS list with a simple, one-level message tree.
- Messages are stored in `data/bbsdb.pkl`.
- Each entry typically includes:
`[id, subject, body, fromNode, timestamp, threadID, replytoID]`
### Posting to Public
To post a public message:
```sh
bbspost $Subject #Message
```
---
## 1.2 Direct Mail (DM) Messages
- **DMs are private messages** sent from one node to another.
- Stored separately from public posts in `data/bbsdm.pkl`.
- Each DM entry typically includes:
`[id, toNode, message, fromNode, timestamp, threadID, replytoID]`
- You can inject DMs directly for automation using the `script/injectDM.py` tool.
### DM Delivery
- To post a DM, use:
```sh
bbspost @USER #Message
```
- When a DM is posted, it is added to the DM database.
- When the bot detects the recipient node on the network, it delivers the DM and then removes it from local storage.
---
### BBS Commands
| 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 |
---
## 2. **Synchronization bot2bot : 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.
## 2.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.
```ini
[bbs]
# The "api" needs enabled which enables file polling
bbsAPI_enabled = True
```
- **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.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).
---
## 2.3. **Scheduling BBS Auto 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)
---
#### 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
```ini
[bbs]
bbslink_enabled = True
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
[scheduler]
enabled = True
interface = 1
channel = 2
value = link
interval = 12 # 12 hours
```
```python
# Custom Schedule Example if using custom for [scheduler]
# Send bbslink looking for peers every 2 days at 10 AM
schedule.every(2).days.at("10:00").do(send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
```
---
---
## 4. **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.
- **Custom file problems?**
- remove the custom_scheduler.py and replace it with [etc/custom_scheduler.py](etc/custom_scheduler.py)
The bbs link command should include `bbslink`
`.do(send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))`
```ini
[bbs]
# The "api" needs enabled which enables file polling and use of `script/injectDM.py`
bbsAPI_enabled = True
```
## 5. **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()`.
---
### 5. **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.
---

View File

@@ -2,32 +2,62 @@
# K7MHI Kelly Keeton 2024
import pickle # pip install pickle
from modules.log import *
from modules.log import logger
from modules.settings import bbs_admin_list, bbs_ban_list, MESSAGE_CHUNK_SIZE, bbs_link_enabled, bbs_link_whitelist, responseDelay
import time
from datetime import datetime
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
useSynchCompression = False
if useSynchCompression:
import zlib
from modules.system import send_raw_bytes
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
# global message list, later we will use a pickle on disk
bbs_messages = []
bbs_dm = []
def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
try:
with open('bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
logger.debug("System: Creating new bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
with open('data/bbsdb.pkl', 'rb') as f:
new_bbs_messages = pickle.load(f)
if isinstance(new_bbs_messages, list):
for msg in new_bbs_messages:
msgHash = hash(tuple(msg[1:3]))
if all(hash(tuple(existing_msg[1:3])) != msgHash for existing_msg in bbs_messages):
new_id = len(bbs_messages) + 1
bbs_messages.append([new_id, msg[1], msg[2], msg[3]])
return True # Loaded successfully, regardless of whether new messages were added
return False # File existed but did not contain a valid list of messages (possibly corrupted)
except FileNotFoundError:
# create a new bbsdb.pkl with a welcome message
# template ([messageID, subject, message, fromNode, now, thread, replyto])
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0,time.strftime('%Y-%m-%d %H:%M:%S'),0,0]]
logger.debug("System: bbsdb.pkl not found, creating new one")
try:
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
return True
except Exception as e:
logger.error(f"System: Error creating bbsdb.pkl: {e}")
return False
except Exception as e:
logger.error(f"System: Error loading bbsdb.pkl: {e}")
return False
def save_bbsdb():
global bbs_messages
# save the bbs messages to the database file
logger.debug("System: Saving bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
try:
logger.debug("System: Saving data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
except Exception as e:
logger.error(f"System: Error saving bbsdb: {e}")
def bbs_help():
# help message
@@ -39,7 +69,7 @@ def bbs_list_messages():
message_list = ""
for message in bbs_messages:
# message[0] is the messageID, message[1] is the subject
message_list += "Msg #" + str(message[0]) + " " + message[1] + "\n"
message_list += "[#" + str(message[0]) + "] " + message[1] + "\n"
# last newline removed
message_list = message_list[:-1]
@@ -69,7 +99,11 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
else:
return "Please specify a message number to delete."
def bbs_post_message(subject, message, fromNode):
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
# post a message to the bbsdb
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
thread = threadID
replyto = replytoID
# post a message to the bbsdb and assign a messageID
messageID = len(bbs_messages) + 1
@@ -77,9 +111,17 @@ def bbs_post_message(subject, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# validate message length isnt three times the MESSAGE_CHUNK_SIZE
if len(message) > (3 * MESSAGE_CHUNK_SIZE):
return "Message too long, max length is " + str(3 * MESSAGE_CHUNK_SIZE) + " characters."
# validate not a duplicate message
for msg in bbs_messages:
if msg[1].strip().lower() == subject.strip().lower() and msg[2].strip().lower() == message.strip().lower():
messageID = msg[0]
return "Message posted. ID is: " + str(messageID)
# validate its not overlength by keeping in chunker limit
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
bbs_messages.append([messageID, subject, message, fromNode, now, thread, replyto])
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
# save the bbsdb
@@ -92,28 +134,34 @@ def bbs_read_message(messageID = 0):
if (messageID - 1) >= len(bbs_messages):
return "Message not found."
if messageID > 0:
fromNode = bbs_messages[messageID - 1][3]
fromNodeHex = hex(fromNode)[-4:]
message = bbs_messages[messageID - 1]
return f"Msg #{message[0]}\nMsg Body: {message[2]}"
return f"Msg #{message[0]}\nFrom:{fromNodeHex}\n{message[2]}"
else:
return "Please specify a message number to read."
def save_bbsdm():
global bbs_dm
# save the bbs messages to the database file
logger.debug("System: Saving Updated BBS Direct Messages bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
logger.debug("System: Saving Updated BBS Direct Messages data/bbsdm.pkl")
with open('data/bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
def load_bbsdm():
global bbs_dm
# load the bbs messages from the database file
try:
with open('bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
with open('data/bbsdm.pkl', 'rb') as f:
new_bbs_dm = pickle.load(f)
if isinstance(new_bbs_dm, list):
for msg in new_bbs_dm:
if msg not in bbs_dm:
bbs_dm.append(msg)
except:
bbs_dm = [[1234567890, "Message", 1234567890]]
logger.debug("System: Creating new bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
logger.debug("System: Creating new data/bbsdm.pkl")
with open('data/bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
def bbs_post_dm(toNode, message, fromNode):
@@ -122,6 +170,14 @@ def bbs_post_dm(toNode, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
return "DM Posted for node " + str(toNode)
# validate message length isnt three times the MESSAGE_CHUNK_SIZE
if len(message) > (3 * MESSAGE_CHUNK_SIZE):
return "Message too long, max length is " + str(3 * MESSAGE_CHUNK_SIZE) + " characters."
# validate not a duplicate message
for msg in bbs_dm:
if msg[0] == int(toNode) and msg[1].strip().lower() == message.strip().lower():
return "DM Posted for node " + str(toNode)
# append the message to the list
bbs_dm.append([int(toNode), message, int(fromNode)])
@@ -130,6 +186,11 @@ def bbs_post_dm(toNode, message, fromNode):
save_bbsdm()
return "BBS DM Posted for node " + str(toNode)
def get_bbs_stats():
global bbs_messages, bbs_dm
# Return some stats on the bbs pending messages and total posted messages
return f"📡BBSdb has {len(bbs_messages)} messages.\nDirect ✉️ Messages waiting: {(len(bbs_dm) - 1)}"
def bbs_check_dm(toNode):
global bbs_dm
# Check for any messages for toNode
@@ -151,6 +212,87 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
def compress_data(data_to_compress):
# Prepare message as bytes
compressed = zlib.compress(data_to_compress.encode('utf-8'))
return compressed
def decompress_data(data_bytes):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
return msg
except Exception as e:
logger.warning(f"Error decompressing data: {e}")
return False
def bbs_receive_compressed(data_bytes, fromNode, RxNode):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
bbs_sync_posts(msg, fromNode, RxNode)
return msg
except Exception as e:
logger.error(f"Error decompressing BBS message: {e}")
return None
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# check if the bbs link is enabled
if bbs_link_whitelist != ['']:
if str(peerNode) not in bbs_link_whitelist:
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
return "System: BBS Link is disabled for your node."
if bbs_link_enabled == False:
return "System: BBS Link is disabled."
# respond when another bot asks for the bbs posts to sync
if "bbslink" in input.lower():
if "$" in input and "#" in input:
#store the message
subject = input.split("$")[1].split("#")[0]
body = input.split("#")[1]
fromNodeHex = input.split("@")[1]
try:
bbs_post_message(subject, body, int(fromNodeHex, 16))
except:
logger.error(f"System: Error parsing bbslink from node {peerNode}: {input}")
fromNodeHex = hex(peerNode)
messageID = input.split(" ")[1]
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
if len(input.split(" ")) > 1:
try:
messageID = int(input.split(" ")[1]) + 1
except:
return "link error"
else:
return "link error"
# send message with delay to keep chutil happy
if messageID < len(bbs_messages):
logger.debug(f"System: wait to bbslink with peer " + str(peerNode))
fromNodeHex = hex(bbs_messages[messageID][3])
time.sleep(5 + responseDelay)
# every 5 messages add extra delay
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
msg = f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
if useSynchCompression:
compressed = compress_data(msg)
send_raw_bytes(peerNode, compressed)
logger.debug("System: Sent compressed bbslink message to peer " + str(peerNode))
else:
return msg
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
#initialize the bbsdb's
load_bbsdb()
load_bbsdm()

442
modules/checklist.md Normal file
View File

@@ -0,0 +1,442 @@
# Enhanced Check-in/Check-out System
## Overview
The enhanced checklist module provides asset tracking and accountability features with advanced safety monitoring capabilities. This system is designed for scenarios where tracking people, equipment, or assets is critical for safety, accountability, or logistics.
## Key Features
### 🔐 Basic Check-in/Check-out
- Simple interface for tracking when people or assets are checked in or out
- Automatic duration calculation
- Location tracking (GPS coordinates if available)
- Notes support for additional context
### ⏰ Safety Monitoring with Time Intervals
- Set expected check-in intervals for safety (minimal 20min)
- Automatic tracking of overdue check-ins
- Ideal for solo activities, remote work, or high-risk operations
- Get alerts when someone hasn't checked in within their expected timeframe
### ✅ Approval Workflow
- Admin approval system for check-ins
- Deny/remove unauthorized check-ins
- Maintain accountability and control
### 📍 Location Tracking
- Automatic GPS location capture when checking in/out
- View last known location in checklist
- **Time Window Monitoring**: Check-in with safety intervals (e.g., `checkin 60 Hunting in tree stand`)
- Tracks if users don't check in within expected timeframe
- Ideal for solo activities, remote work, or safety accountability
- Provides `get_overdue_checkins()` function for alert integration
- **Approval Workflow**:
- `clok <id>` - Approve pending check-ins (admin)
- `denycl <id>` - Deny/remove check-ins (admin)
- Support for approval-based workflows
#### New Commands:
- `clok <id>` - Approve a check-in
- `denycl <id>` - Deny a check-in
- Enhanced `checkin [interval] [note]` - Now supports interval parameter
### Enhanced Check Out Options
You can now check out in three ways:
#### 1. Check Out the Most Recent Active Check-in
```
checkout [notes]
```
Checks out your most recent active check-in.
*Example:*
```
checkout Heading back to camp
```
#### 2. Check Out All Active Check-ins
```
checkout all [notes]
```
Checks out **all** of your active check-ins at once.
*Example:*
```
checkout all Done for the day
```
*Response:*
```
Checked out 2 check-ins for Hunter1. Durations: 01:23:45, 00:15:30
```
#### 3. Check Out a Specific Check-in by ID
```
checkout <checkin_id> [notes]
```
Checks out a specific check-in using its ID (as shown in the `checklist` command).
*Example:*
```
checkout 123 Leaving early
```
*Response:*
```
Checked out check-in ID 123 for Hunter1. Duration: 00:45:12
```
**Tip:**
- Use `checklist` to see your current check-in IDs and durations.
- You can always add a note to any checkout command for context.
---
These options allow you to manage your check-ins more flexibly, whether you want to check out everything at once or just a specific session.
## Configuration
Add to your `config.ini`:
```ini
[checklist]
enabled = True
checklist_db = data/checklist.db
# Set to True to reverse the meaning of checkin/checkout
reverse_in_out = False
```
## Commands Reference
### Basic Commands
#### Check In
```
checkin [interval] [notes]
```
Check in to the system. Optionally specify a monitoring interval in minutes.
**Examples:**
```
checkin Arrived at base camp
checkin 30 Solo hiking on north trail
checkin 60 Working alone in tree stand
checkin Going hunting
```
#### Check Out
```
checkout [notes]
```
Check out from the system. Shows duration since check-in.
**Examples:**
```
checkout Heading back
checkout Mission complete
checkout
```
#### View Checklist
```
checklist
```
Shows all active check-ins with durations.
**Example Response:**
```
ID: Hunter1 checked-In for 01:23:45📝Solo hunting
ID: Tech2 checked-In for 00:15:30📝Equipment repair
```
### Admin Commands
#### Approve Check-in
```
approvecl <checkin_id>
```
Approve a pending check-in (requires admin privileges).
**Example:**
```
approvecl 123
```
#### Deny Check-in
```
denycl <checkin_id>
```
Deny and remove a check-in (requires admin privileges).
**Example:**
```
denycl 456
```
## Safety Monitoring Feature
### How Time Intervals Work
When checking in with an interval parameter, the system will track whether you check in again or check out within that timeframe.
```
checkin 60 Hunting in remote area
```
This tells the system:
- You're checking in now
- You expect to check in again or check out within 60 minutes
- If 60 minutes pass without activity, you'll be marked as overdue alert
### Use Cases for Time Intervals
1. **Solo Activities**: Hunting, hiking, or working alone
```
checkin 30 Solo patrol north sector
```
2. **High-Risk Operations**: Tree work, equipment maintenance
```
checkin 45 Climbing tower for antenna work
```
3. **Remote Work**: Working in isolated areas
```
checkin 120 Survey work in remote canyon
```
4. **Check-in Points**: Regular status updates during long operations
```
checkin 15 Descending cliff
```
5. **Check-in a reminder**: Reminders to check in on something like a pot roast
```
checkin 30 🍠🍖
```
### Overdue Check-ins
The system tracks all check-ins with time intervals and can identify who is overdue. The module provides the `get_overdue_checkins()` function that returns a list of overdue users. It alerts on the 20min watchdog.
## Practical Examples
### Example 1: Hunting Scenario
Hunter checks in before going into the field:
```
checkin 60 Hunting deer stand #3, north 40
```
System response:
```
Checked✅In: Hunter1 (monitoring every 60min)
```
If the hunter doesn't check out or check in again within 60 minutes, they will appear on the overdue list.
When done hunting:
```
checkout Heading back to camp
```
System response:
```
Checked⌛Out: Hunter1 duration 02:15:30
```
### Example 2: Emergency Response Team
Team leader tracks team members:
```
# Team members check in
checkin 30 Search grid A-1
checkin 30 Search grid A-2
checkin 30 Search grid A-3
```
Team leader views status:
```
checklist
```
Response shows all active searchers with their durations.
### Example 3: Equipment Checkout
Track equipment loans:
```
checkin Radio #5 for field ops
```
When equipment is returned:
```
checkout Equipment returned
```
### Example 4: Site Survey
Field technicians checking in at locations:
```
# At first site
checkin 45 Site survey tower location 1
# Moving to next site (automatically checks out from first)
checkin 45 Site survey tower location 2
```
## Integration with Other Systems
### Geo-Location Awareness
The checklist system automatically captures GPS coordinates when available. This can be used for:
- Tracking last known position
- Asset location management
### Alert Systems
The overdue check-in feature can trigger:
- Notifications to supervisors
- Automated messages to response teams
- Email/SMS notifications (if configured)
### Scheduling Integration
Combine with the scheduler module to:
- Send reminders to check in
- Schedule periodic check-in requirements
## Best Practices
### For Users
1. **Always Include Context**: Add notes when checking in
```
checkin 30 North trail maintenance
```
Not just:
```
checkin
```
2. **Set Realistic Intervals**: Don't set intervals too short or too long
- Too short: False alarms
- Too long: Defeats safety purpose
3. **Check Out Promptly**: Always check out when done to clear your status
4. **Use Consistent Naming**: If tracking equipment, use consistent names
### For Administrators
1. **Review Checklist Regularly**: Monitor who is checked in
```
checklist
```
The list will show ✅ approved and ☑️ unapproved
The alarm will only alert on approved.
in config.ini
```ini
# Auto approve new checklists
auto_approve = True
# Check-in reminder interval is 5min
# Checkin broadcast interface and channel is emergency_handler interface and channel
```
2. **Respond to Overdue Situations**: Act on overdue check-ins promptly
3. **Set Clear Policies**: Establish when and how to use the system
4. **Train Users**: Ensure everyone knows how to use time intervals
5. **Test the System**: Regularly verify the system is working
## Safety Scenarios
### Scenario 1: Tree Stand Hunting
```
checkin 60 Hunting from tree stand at north plot
```
If hunter falls or has medical emergency, they'll be marked overdue after 60 minutes.
### Scenario 2: Equipment Maintenance
```
checkin 30 Generator maintenance at remote site
```
If technician encounters danger, overdue status can be detected. Note: Requires alert system integration to send notifications.
### Scenario 3: Hiking
```
checkin 120 Day hike to mountain peak
```
Longer interval for extended activity, but still provides safety net.
### Scenario 4: Watchstanding
```
checkin 240 Night watch duty
```
Regular check-ins every 4 hours ensure person is alert and safe.
## Database Schema
### checkin Table
```sql
CREATE TABLE checkin (
checkin_id INTEGER PRIMARY KEY,
checkin_name TEXT,
checkin_date TEXT,
checkin_time TEXT,
location TEXT,
checkin_notes TEXT,
approved INTEGER DEFAULT 1,
expected_checkin_interval INTEGER DEFAULT 0
)
```
### checkout Table
```sql
CREATE TABLE checkout (
checkout_id INTEGER PRIMARY KEY,
checkout_name TEXT,
checkout_date TEXT,
checkout_time TEXT,
location TEXT,
checkout_notes TEXT
)
```
## Reverse Mode
Setting `reverse_in_out = True` in config swaps the meaning of checkin and checkout commands. This is useful if your organization uses opposite terminology.
With `reverse_in_out = True`:
- `checkout` command performs a check-in
- `checkin` command performs a check-out
## Migration from Basic Checklist
The enhanced checklist is backward compatible with the basic version. Existing check-ins will continue to work, and new features are optional. The database will automatically upgrade to add new columns when first accessed.
## Troubleshooting
### Not Seeing Overdue Alerts
The overdue detection is built into the module, but alerts need to be configured in the main bot scheduler. Check your scheduler configuration.
### Wrong Duration Shown
Duration is calculated from check-in time to current time. If system clock is wrong, durations will be incorrect. Ensure system time is accurate.
### Can't Approve/Deny Check-ins
These are admin-only commands. Check that your node ID is in the `bbs_admin_list`.
## Support
For issues or feature requests, please file an issue on the GitHub repository.

471
modules/checklist.py Normal file
View File

@@ -0,0 +1,471 @@
# Checkin Checkout database module for the bot
# K7MHI Kelly Keeton 2024
import sqlite3
from modules.log import logger
from modules.settings import checklist_db, reverse_in_out, bbs_ban_list, bbs_admin_list, checklist_auto_approve
import time
trap_list_checklist = ("checkin", "checkout", "checklist", "approvecl", "denycl",)
def initialize_checklist_database():
try:
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
logger.debug("System: Checklist: Initializing database...")
c.execute('''CREATE TABLE IF NOT EXISTS checkin
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT,
checkin_time TEXT, location TEXT, checkin_notes TEXT,
approved INTEGER DEFAULT 1, expected_checkin_interval INTEGER DEFAULT 0,
removed INTEGER DEFAULT 0)''')
c.execute('''CREATE TABLE IF NOT EXISTS checkout
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT,
checkout_time TEXT, location TEXT, checkout_notes TEXT,
checkin_id INTEGER, removed INTEGER DEFAULT 0)''')
conn.commit()
conn.close()
return True
except Exception as e:
logger.error(f"Checklist: Failed to initialize database: {e} Please delete old checklist database file. rm data/checklist.db")
return False
def checkin(name, date, time, location, notes):
location = ", ".join(map(str, location))
# Auto-approve if setting is enabled
approved_value = 1 if checklist_auto_approve else 0
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute(
"INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes, removed, approved) VALUES (?, ?, ?, ?, ?, 0, ?)",
(name, date, time, location, notes, approved_value)
)
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_checklist_database()
c.execute(
"INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes, removed, approved) VALUES (?, ?, ?, ?, ?, 0, ?)",
(name, date, time, location, notes, approved_value)
)
else:
raise
conn.commit()
conn.close()
if reverse_in_out:
return "Checked✅Out: " + str(name)
else:
return "Checked✅In: " + str(name)
def checkout(name, date, time_str, location, notes, all=False, checkin_id=None):
location = ", ".join(map(str, location))
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
checked_out_ids = []
durations = []
try:
if checkin_id is not None:
# Check out a specific check-in by ID
c.execute("""
SELECT checkin_id, checkin_time, checkin_date FROM checkin
WHERE checkin_id = ? AND checkin_name = ?
""", (checkin_id, name))
row = c.fetchone()
if row:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes, checkin_id) VALUES (?, ?, ?, ?, ?, ?)",
(name, date, time_str, location, notes, row[0]))
checkin_time, checkin_date = row[1], row[2]
checkin_datetime = time.strptime(checkin_date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
durations.append(time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds)))
checked_out_ids.append(row[0])
elif all:
# Check out all active check-ins for this user
c.execute("""
SELECT checkin_id, checkin_time, checkin_date FROM checkin
WHERE checkin_name = ?
AND removed = 0
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout WHERE checkin_id IS NOT NULL
)
""", (name,))
rows = c.fetchall()
for row in rows:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes, checkin_id) VALUES (?, ?, ?, ?, ?, ?)",
(name, date, time_str, location, notes, row[0]))
checkin_time, checkin_date = row[1], row[2]
checkin_datetime = time.strptime(checkin_date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
durations.append(time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds)))
checked_out_ids.append(row[0])
else:
# Default: check out the most recent active check-in
c.execute("""
SELECT checkin_id, checkin_time, checkin_date FROM checkin
WHERE checkin_name = ?
AND removed = 0
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout WHERE checkin_id IS NOT NULL
)
ORDER BY checkin_date DESC, checkin_time DESC
LIMIT 1
""", (name,))
row = c.fetchone()
if row:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes, checkin_id) VALUES (?, ?, ?, ?, ?, ?)",
(name, date, time_str, location, notes, row[0]))
checkin_time, checkin_date = row[1], row[2]
checkin_datetime = time.strptime(checkin_date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
durations.append(time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds)))
checked_out_ids.append(row[0])
except sqlite3.OperationalError as e:
if "no such table" in str(e):
conn.close()
initialize_checklist_database()
return checkout(name, date, time_str, location, notes, all=all, checkin_id=checkin_id)
else:
conn.close()
raise
conn.commit()
conn.close()
if checked_out_ids:
if all:
return f"Checked out {len(checked_out_ids)} check-ins for {name}. Durations: {', '.join(durations)}"
elif checkin_id is not None:
return f"Checked out check-in ID {checkin_id} for {name}. Duration: {durations[0]}"
else:
if reverse_in_out:
return f"Checked⌛In: {name} duration {durations[0]}"
else:
return f"Checked⌛Out: {name} duration {durations[0]}"
else:
return f"None found for {name}"
def approve_checkin(checkin_id):
"""Approve a pending check-in"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("UPDATE checkin SET approved = 1 WHERE checkin_id = ?", (checkin_id,))
if c.rowcount == 0:
conn.close()
return f"Check-in ID {checkin_id} not found."
conn.commit()
conn.close()
return f"✅ Check-in {checkin_id} approved."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error approving check-in: {e}")
return "Error approving check-in."
def deny_checkin(checkin_id):
"""Deny/delete a pending check-in"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("DELETE FROM checkin WHERE checkin_id = ?", (checkin_id,))
if c.rowcount == 0:
conn.close()
return f"Check-in ID {checkin_id} not found."
conn.commit()
conn.close()
return f"❌ Check-in {checkin_id} denied and removed."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error denying check-in: {e}")
return "Error denying check-in."
def set_checkin_interval(name, interval_minutes):
"""Set expected check-in interval for a user (for safety monitoring)"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
# Update the most recent active check-in for this user
c.execute("""
UPDATE checkin
SET expected_checkin_interval = ?
WHERE checkin_name = ?
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
ORDER BY checkin_date DESC, checkin_time DESC
LIMIT 1
""", (interval_minutes, name))
if c.rowcount == 0:
conn.close()
return f"No active check-in found for {name}."
conn.commit()
conn.close()
return f"⏰ Check-in interval set to {interval_minutes} minutes for {name}."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error setting check-in interval: {e}")
return "Error setting check-in interval."
def get_overdue_checkins():
"""Get list of users who haven't checked in within their expected interval"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
current_time = time.time()
try:
c.execute("""
SELECT checkin_id, checkin_name, checkin_date, checkin_time, expected_checkin_interval, location, checkin_notes
FROM checkin
WHERE expected_checkin_interval > 0
AND approved = 1
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
""")
active_checkins = c.fetchall()
conn.close()
overdue_list = []
for checkin_id, name, date, time_str, interval, location, notes in active_checkins:
checkin_datetime = time.mktime(time.strptime(f"{date} {time_str}", "%Y-%m-%d %H:%M:%S"))
time_since_checkin = (current_time - checkin_datetime) / 60 # in minutes
if time_since_checkin > interval:
overdue_minutes = int(time_since_checkin - interval)
overdue_list.append({
'id': checkin_id,
'name': name,
'location': location,
'overdue_minutes': overdue_minutes,
'interval': interval,
'checkin_notes': notes
})
return overdue_list
except sqlite3.OperationalError as e:
conn.close()
if "no such table" in str(e):
initialize_checklist_database()
return get_overdue_checkins()
logger.error(f"Checklist: Error getting overdue check-ins: {e}")
return []
def format_overdue_alert():
header = "⚠️ OVERDUE CHECK-INS:\a\n"
alert = ""
try:
"""Format overdue check-ins as an alert message"""
overdue = get_overdue_checkins()
if not overdue:
return None
for entry in overdue:
hours = entry['overdue_minutes'] // 60
minutes = entry['overdue_minutes'] % 60
if hours > 0:
alert += f"{entry['name']}: {hours}h {minutes}m overdue"
else:
alert += f"{entry['name']}: {minutes}m overdue"
# if entry['location']:
# alert += f" @ {entry['location']}"
if entry['checkin_notes']:
alert += f" 📝{entry['checkin_notes']}"
alert += "\n"
if alert:
return header + alert.rstrip()
except Exception as e:
logger.error(f"Checklist: Error formatting overdue alert: {e}")
return None
def list_checkin():
# list checkins
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("""
SELECT * FROM checkin
WHERE removed = 0
AND NOT EXISTS (
SELECT 1 FROM checkout
WHERE checkout.checkin_id = checkin.checkin_id
)
""")
rows = c.fetchall()
except sqlite3.OperationalError as e:
if "no such table" in str(e):
conn.close()
initialize_checklist_database()
return list_checkin()
else:
conn.close()
initialize_checklist_database()
return "Error listing checkins."
conn.close()
# Get overdue info
overdue = {entry['id']: entry for entry in get_overdue_checkins()}
checkin_list = ""
for row in rows:
checkin_id = row[0]
# Calculate length of time checked in, including days
total_seconds = time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))
days = int(total_seconds // 86400)
hours = int((total_seconds % 86400) // 3600)
minutes = int((total_seconds % 3600) // 60)
seconds = int(total_seconds % 60)
if days > 0:
timeCheckedIn = f"{days}d {hours:02}:{minutes:02}:{seconds:02}"
else:
timeCheckedIn = f"{hours:02}:{minutes:02}:{seconds:02}"
# Add ⏰ if routine check-ins are required
routine = ""
if len(row) > 7 and row[7] and int(row[7]) > 0:
routine = f" ⏰({row[7]}m)"
# Indicate approval status
approved_marker = "" if row[6] == 1 else "☑️"
# Check if overdue
if checkin_id in overdue:
overdue_minutes = overdue[checkin_id]['overdue_minutes']
overdue_hours = overdue_minutes // 60
overdue_mins = overdue_minutes % 60
if overdue_hours > 0:
overdue_str = f"overdue by {overdue_hours}h {overdue_mins}m"
else:
overdue_str = f"overdue by {overdue_mins}m"
status = f"{row[1]} {overdue_str}{routine}"
else:
status = f"{row[1]} checked-In for {timeCheckedIn}{routine}"
checkin_list += f"ID: {checkin_id} {approved_marker} {status}"
if row[5] != "":
checkin_list += " 📝" + row[5]
if row != rows[-1]:
checkin_list += "\n"
# if empty list
if checkin_list == "":
return "No data to display."
return checkin_list
def process_checklist_command(nodeID, message, name="none", location="none"):
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
# if user on bbs_ban_list reject command
if str(nodeID) in bbs_ban_list:
logger.warning("System: Checklist attempt from the ban list")
return "unable to process command"
is_admin = False
if str(nodeID) in bbs_admin_list:
is_admin = True
message_lower = message.lower()
parts = message.split()
try:
comment = message.split(" ", 1)[1] if len(parts) > 1 else ""
except IndexError:
comment = ""
# handle checklist commands
if ("checkin" in message_lower and not reverse_in_out) or ("checkout" in message_lower and reverse_in_out):
# Check if interval is specified: checkin 60 comment
interval = 0
actual_comment = comment
if comment and parts[1].isdigit():
interval = int(parts[1])
actual_comment = " ".join(parts[2:]) if len(parts) > 2 else ""
result = checkin(name, current_date, current_time, location, actual_comment)
# Set interval if specified
if interval > 0:
set_checkin_interval(name, interval)
result += f" (monitoring every {interval}min)"
return result
elif ("checkout" in message_lower and not reverse_in_out) or ("checkin" in message_lower and reverse_in_out):
# Support: checkout all, checkout <id>, or checkout [note]
all_flag = False
checkin_id = None
actual_comment = comment
# Split the command into parts after the keyword
checkout_args = parts[1:] if len(parts) > 1 else []
if checkout_args:
if checkout_args[0].lower() == "all":
all_flag = True
actual_comment = " ".join(checkout_args[1:]) if len(checkout_args) > 1 else ""
elif checkout_args[0].isdigit():
checkin_id = int(checkout_args[0])
actual_comment = " ".join(checkout_args[1:]) if len(checkout_args) > 1 else ""
else:
actual_comment = " ".join(checkout_args)
return checkout(name, current_date, current_time, location, actual_comment, all=all_flag, checkin_id=checkin_id)
# elif "purgein" in message_lower:
# return mark_checkin_removed_by_name(name)
# elif "purgeout" in message_lower:
# return mark_checkout_removed_by_name(name)
elif "approvecl " in message_lower:
if not is_admin:
return "You do not have permission to approve check-ins."
try:
checkin_id = int(parts[1])
return approve_checkin(checkin_id)
except (ValueError, IndexError):
return "Usage: checklistapprove <checkin_id>"
elif "denycl " in message_lower:
if not is_admin:
return "You do not have permission to deny check-ins."
try:
checkin_id = int(parts[1])
return deny_checkin(checkin_id)
except (ValueError, IndexError):
return "Usage: checklistdeny <checkin_id>"
elif "?" in message_lower:
if not reverse_in_out:
return ("Command: checklist followed by\n"
"checkin [interval] [note]\n"
"checkout [all] [note]\n"
"Example: checkin 60 Leaving for a hike")
else:
return ("Command: checklist followed by\n"
"checkout [all] [interval] [note]\n"
"checkin [note]\n"
"Example: checkout 60 Leaving for a hike")
elif message_lower.strip() == "checklist":
return list_checkin()
else:
return "Invalid command."
def mark_checkin_removed_by_name(name):
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
c.execute("UPDATE checkin SET removed = 1 WHERE checkin_name = ?", (name,))
affected = c.rowcount
conn.commit()
conn.close()
return f"Marked {affected} check-in(s) as removed for {name}."
def mark_checkout_removed_by_name(name):
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
c.execute("UPDATE checkout SET removed = 1 WHERE checkout_name = ?", (name,))
affected = c.rowcount
conn.commit()
conn.close()
return f"Marked {affected} checkout(s) as removed for {name}."

202
modules/dxspot.py Normal file
View File

@@ -0,0 +1,202 @@
# meshing-around modules/dxspot.py - Handles DX Spotter integration
# Fetches DX spots from Spothole API based on user commands
# 2025 K7MHI Kelly Keeton
import requests
import datetime
from modules.log import logger
from modules.settings import latitudeValue, longitudeValue
trap_list_dxspotter = ["dx"]
def handledxcluster(message, nodeID, deviceID):
from modules.dxspot import get_spothole_spots
if "DX" in message.upper():
logger.debug(f"System: DXSpotter: Device:{deviceID} Handler: DX Spot Request Received from Node {nodeID}")
band = None
mode = None
source = None
dx_call = None
parts = message.split()
for part in parts:
if part.lower().startswith("band="):
band = part.split("=")[1]
elif part.lower().startswith("mode="):
mode = part.split("=")[1]
elif part.lower().startswith("ota="):
source = part.split("=")[1]
elif part.lower().startswith("of="):
dx_call = part.split("=")[1]
# Build params dict
params = {}
if source:
params["source"] = source.upper()
if band:
params["band"] = band.lower()
if mode:
params["mode"] = mode.upper()
if dx_call:
params["dx_call"] = dx_call.upper()
# Fetch spots
spots = get_spothole_spots(**params)
if spots:
response_lines = []
for spot in spots[:5]:
callsign = spot.get('dx_call', spot.get('callsign', 'N/A'))
freq_hz = spot.get('freq', spot.get('frequency', None))
frequency = f"{float(freq_hz)/1e6:.3f} MHz" if freq_hz else "N/A"
mode_val = spot.get('mode', 'N/A')
comment = spot.get('comment', '')
if len(comment) > 111: # Truncate comment to 111 chars
comment = comment[:111] + '...'
sig = spot.get('sig', '')
de_grid = spot.get('de_grid', '')
de_call = spot.get('de_call', '')
sig_ref_name = spot.get('sig_refs_names', [''])[0] if spot.get('sig_refs_names') else ''
line = f"{callsign} @{frequency} {mode_val} {sig} {sig_ref_name} by:{de_call} {de_grid} {comment}"
response_lines.append(line)
response = "\n".join(response_lines)
else:
response = "No DX spots found."
return response
return "Error: No DX command found."
def get_spothole_spots(source=None, band=None, mode=None, date=None, dx_call=None, de_continent=None, de_location=None):
"""
Fetches spots from https://spothole.app/api/v1/spots with optional filters.
Returns a list of spot dicts.
"""
url = "https://spothole.app/api/v1/spots"
params = {}
fetched_count = 0
# Add administrative filters if provided
qrt = False # Always fetch active spots
needs_sig = False # Always need spots wth a group ike Xota
limit = 4
dedupe = True
params["dedupe"] = str(dedupe).lower()
params["limit"] = limit
params["qrt"] = str(qrt).lower()
params["needs_sig"] = str(needs_sig).lower()
params["needs_sig_ref"] = 'true'
# Only get spots from last 9 hours
received_since_dt = datetime.datetime.utcnow() - datetime.timedelta(hours=9)
received_since = int(received_since_dt.timestamp())
params["received_since"] = received_since
# Add spot filters if provided
if source:
params["source"] = source
if band:
params["band"] = band
if mode:
params["mode"] = mode
if date:
# date should be a string in YYYY-MM-DD or datetime.date
if isinstance(date, datetime.date):
params["date"] = date.isoformat()
else:
params["date"] = date
try:
headers = {"User-Agent": "meshing-around-dxspotter/1.0"}
response = requests.get(url, params=params, headers=headers)
response.raise_for_status()
spots = response.json()
except Exception as e:
logger.debug(f"Error fetching spots: {e}")
spots = []
fetched_count = len(spots)
# Admin Filters done via config.ini
de_grid = None # e.g., "EM00"
de_dxcc_id = None # e.g., "291"
de_call = None # e.g., "K7MHI"
dx_itu_zone = None # e.g., "3"
dx_cq_zone = None # e.g., "4"
dx_dxcc_id = None # e.g., "291"
# spotter filters
# location filter
de_latitude = None # e.g., 34.05
de_longitude = None # e.g., -118.25
if de_location:
de_latitude, de_longitude = de_location
elif de_latitude is not None and de_longitude is not None:
de_latitude = latitudeValue
de_longitude = longitudeValue
if de_latitude and de_longitude:
lat_range = (de_latitude - 1.0, de_latitude + 1.0)
lon_range = (de_longitude - 1.0, de_longitude + 1.0)
spots = [spot for spot in spots if lat_range[0] <= spot.get('de_latitude', 0) <= lat_range[1] and
lon_range[0] <= spot.get('de_longitude', 0) <= lon_range[1]]
# grid filter
if de_grid:
spots = [spot for spot in spots if spot.get('de_grid', '').upper() == de_grid.upper()]
# DXCC Filters
if de_dxcc_id:
spots = [spot for spot in spots if str(spot.get('de_dxcc_id', '')) == str(de_dxcc_id)]
# By reporting callsign
if de_call:
spots = [spot for spot in spots if spot.get('de_call', '').upper() == de_call.upper()]
# DX spotted in zone
if dx_itu_zone:
spots = [spot for spot in spots if str(spot.get('dx_itu_zone', '')) == str(dx_itu_zone)]
if dx_cq_zone:
spots = [spot for spot in spots if str(spot.get('dx_cq_zone', '')) == str(dx_cq_zone)]
if dx_dxcc_id:
spots = [spot for spot in spots if str(spot.get('dx_dxcc_id', '')) == str(dx_dxcc_id)]
# User Runtime Filters
# Filter by dx_call if provided
if dx_call:
spots = [spot for spot in spots if spot.get('dx_call', '').upper() == dx_call.upper()]
# Filter by de_continent if provided
if de_continent:
spots = [spot for spot in spots if spot.get('de_continent', '').upper() == de_continent.upper()]
# Filter by de_location if provided
if de_location:
spots = [spot for spot in spots if spot.get('de_location', '').upper() == de_location.upper()]
logger.debug(f"System: Spothole Returning {len(spots)} spots after filtering (fetched {fetched_count})")
return spots
def handle_post_dxspot():
time = int(datetime.datetime.utcnow().timestamp())
freq = 14200000 # 14 MHz
comment = "Test spot please ignore"
de_spot = "N0CALL"
dx_spot = "N0CALL"
spot = {"dx_call": dx_spot, "time": time, "freq": freq, "comment": comment, "de_call": de_spot}
try:
success = post_spothole_spot(spot)
if success:
return "Spot posted successfully."
else:
return "Failed to post spot."
except Exception as e:
logger.debug(f"Error in handle_post_dxspot: {e}")
return "Error occurred while posting spot."
def post_spothole_spot(spot):
"""
Posts a new spot to https://spothole.app/api/v1/spot.
"""
url = "https://spothole.app/api/v1/spot"
headers = {"Content-Type": "application/json", "User-Agent": "meshing-around-dxspotter/1.0"}
try:
response = requests.post(url, json=spot, headers=headers, timeout=10)
response.raise_for_status()
logger.debug(f"Spot posted successfully: {response.json()}")
return True
except Exception as e:
logger.debug(f"Error posting spot: {e}")
return False

196
modules/filemon.py Normal file
View File

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

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

@@ -0,0 +1,751 @@
# 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)
- [Hangman](#hangman-game-module)
- [Quiz](#quiz-game-module)
- [Survey](#survey--module-game)
- [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"
]
# Hangman Game Module
A classic word-guessing game for the Meshtastic mesh-bot. Try to guess the hidden word one letter at a time before you run out of chances!
## How to Play
- **Start the Game:**
Send the command `hangman` via DM to the bot to begin a new game.
- **Objective:**
Guess the secret word by suggesting letters, one at a time. Each incorrect guess brings you closer to losing!
- **Game Flow:**
1. **New Game:**
- The bot picks a random word and shows you its masked form (e.g., `_ _ _ _ _`).
- Youll see your total games played and games won.
2. **Guessing:**
- Type a single letter to guess.
- Correct guesses reveal all instances of that letter in the word.
- Incorrect guesses are tracked; you have 6 chances before the game ends.
- The bot shows your progress, wrong guesses, and a hangman emoji status.
3. **Winning & Losing:**
- Guess all letters before reaching 6 wrong guesses to win!
- If you lose, the bot reveals the word and starts a new game.
- **Commands:**
- Enter a single letter to guess.
- Start a new game by sending `hangman` again.
## Example Session
```
_ _ _ _ _ _ _
Guess a letter
🥳
Total Games: 1, Won: 1
M E S H T A S T I C
Guess a letter
```
## Notes
- The word list is loaded from `data/hangman.json` if available, or uses a built-in default list. [\"apple\",\"banana\",\"cherry\"]
- Game stats are tracked per player.
- Only one game session per player at a time.
- Play via DM for best experience.
## Data Files
- `data/hangman.json`: List of words for Hangman.
Example:
```
[
"apple",
"banana",
"cherry"
]
```
## Credits
- Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
# Quiz Game Module
This module implements a multiplayer quiz game for the Meshtastic mesh-bot.
## How to Play
- **Start the Game:**
The quizmaster starts the quiz session (usually with `/quiz start` or similar command).
- **Join the Game:**
Players join by sending `/quiz join` or by answering a question while a quiz is active.
- **Answer Questions:**
- Use `Q: <answer>` to answer the current question.
- For multiple choice, answer with `A`, `B`, `C`, etc.
- For free-text, type the answer after `Q: `.
- Use `Q: ?` to request the next question.
- **Leave the Game:**
Players can leave at any time with `/quiz leave`.
- **Stop the Game:**
The quizmaster stops the quiz session (e.g., `/quiz stop`). Final scores and the top 3 players are announced.
## Rules & Features
- Only the quizmaster can start or stop the quiz.
- Players can join or leave at any time while the quiz is active.
- Questions are loaded from quiz_questions.json and can be multiple choice or free-text.
- Players earn 1 point for each correct answer.
- The first player to answer each question correctly is noted.
- The top 3 players are displayed at the end of the quiz.
- The quizmaster can broadcast messages to all players.
## Example Commands
- Start quiz:
`/quiz start`
- Join quiz:
`/quiz join`
- Answer a question:
`Q: B`
`Q: Paris`
- Next question:
`Q: ?`
- Leave quiz:
`/quiz leave`
- Stop quiz:
`/quiz stop`
## Notes
- Only one quiz can be active at a time.
- Players can only answer each question once.
- The quizmaster is defined by the `bbs_admin_list` variable.
- Questions must be formatted correctly in the JSON file for the game to function.
---
**Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025**
Certainly! Heres documentation for the **Survey Game Module** in the same format as your other game modules:
---
# Survey Module "game"
This module implements a survey system for the Meshtastic mesh-bot.
## How to Play
- **Start the Survey:**
Users start a survey by specifying the survey name (e.g., `/survey start example`).
The survey will prompt the user with the first question.
- **Answer Questions:**
- For multiple choice: reply with a letter (A, B, C, ...).
- For integer: reply with a number.
- For text: reply with your answer as text.
After each answer, the next question is shown automatically.
- **End the Survey:**
The survey ends automatically after the last question, or the user can send `end` to finish early.
Responses are saved to a CSV file.
## Rules & Features
- Surveys are defined in JSON files in surveys (e.g., `example_survey.json`).
- Each survey can have multiple choice, integer, or text questions.
- User responses are saved to a CSV file named `<survey_name>_responses.csv` in the same directory.
- Users can only answer each question once per survey session.
- Survey results can be summarized and reported by the bot.
## Example Commands
- Start a survey:
`/survey start example`
- Answer a multiple choice question:
`A`
- Answer an integer question:
`42`
- Answer a text question:
`My favorite color is blue.`
- End the survey early:
`end`
- Get survey results (admin):
`/survey results example`
## Notes
- Only surveys listed in the surveys directory with the `_survey.json` suffix are available.
- Each users responses are tracked separately.
- Results are summarized and can be displayed by the bot.
---
**Written for Meshtastic mesh-bot by K7MHI Kelly Keeton 2025**
___
Pay no attention to the..
'pygame - Community Edition' ('pygame-ce' for short) is a fork of the original 'pygame' library by former 'pygame' core contributors.
It offers many new features and optimizations, receives much better maintenance and runs under a better governance model, while being highly compatible with code written for upstream pygame (`import pygame` still works).
**Details**
- [Initial announcement on Reddit](<https://www.reddit.com/r/pygame/comments/1112q10/pygame_community_edition_announcement/>) (or https://discord.com/channels/772505616680878080/772506385304649738/1074593440148500540)
- [Why the forking happened](<https://www.reddit.com/r/pygame/comments/18xy7nf/what_was_the_disagreement_that_led_to_pygamece/>)
**Helpful Links**
- https://discord.com/channels/772505616680878080/772506385304649738
- [Our GitHub releases](<https://github.com/pygame-community/pygame-ce/releases>)
- [Our docs](https://pyga.me/docs/)
**Installation**
```sh
pip uninstall pygame # Uninstall pygame first since it would conflict with pygame-ce
pip install pygame-ce
```
-# Because 'pygame' installs to the same location as 'pygame-ce', it must first be uninstalled.
-# Note that the `import pygame` syntax has not changed with pygame-ce.
---

View File

@@ -2,12 +2,12 @@
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
from random import choices, shuffle
from modules.log import *
from modules.log import logger
from modules.settings import jackTracker
import time
import pickle
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}]
SUITS = ("♥️", "♦️", "♠️", "♣️")
RANKS = (
@@ -113,33 +113,35 @@ class jackChips:
self.total -= self.bet
self.winnings -= 1
def take_bet(bet_amount, player_money):
try:
if bet_amount >= player_money or bet_amount <= 0:
return f"Enter a bet amount between 1 and {player_money}"
return bet_amount
def success_rate(next_card, player_hand):
# Estimate the chance of a successful 'HIT' (not busting) in blackjack.
except TypeError:
return "Invalid bet amount"
# If player already has 21 or more, hitting will always bust
if player_hand.value >= 21:
return "\n🧠 What do you think?"
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
# Calculate how much more the player can add without busting
max_safe = 21 - player_hand.value
if rate < 100:
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
elif rate > 100:
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"
else:
msg += f"If Hit, a low chance of success."
return msg
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]]
@@ -157,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):
@@ -216,9 +218,33 @@ def setLastCmdJack(nodeID, cmd):
return True
return False
def playBlackJack(nodeID, message):
def saveHSJack(nodeID, highScore):
# Save the game state to pickle
highScore = {'nodeID': nodeID, 'highScore': highScore}
try:
with open('data/blackjack_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new data/blackjack_hs.pkl file")
with open('data/blackjack_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
def loadHSJack():
try:
with open('data/blackjack_hs.pkl', 'rb') as file:
highScore = pickle.load(file)
return highScore
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new data/blackjack_hs.pkl file")
highScore = {'nodeID': 0, 'highScore': 0}
with open('data/blackjack_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
return 0
def playBlackJack(nodeID, message, last_cmd=None):
# Initalize the Game
msg, last_cmd = '', None
blackJack = False
p_win, d_win, draw = 0, 0, 0
p_chips = jackChips()
p_hand = jackHand()
@@ -243,8 +269,8 @@ def playBlackJack(nodeID, message):
d_win = jackTracker[i]['gameStats']['d_win']
draw = jackTracker[i]['gameStats']['draw']
bet_money = jackTracker[i]['bet']
p_chips.bet = bet_money
if last_cmd == "playing":
p_chips.bet = bet_money
p_cards = jackTracker[i]['p_cards']
d_cards = jackTracker[i]['d_cards']
p_hand = jackTracker[i]['p_hand']
@@ -253,45 +279,51 @@ def playBlackJack(nodeID, message):
if last_cmd is None:
# create new player if not in tracker
logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"Welcome to BlackJack!♠️♥️♣️♦️ you have {p_chips.total} chips, Whats your bet?"
if nodeID != 0:
#logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"You have {p_chips.total} chips. Whats your bet?"
return "Error: Player not found."
if getLastCmdJack(nodeID) == "new":
# Place Bet
try:
# handle B letter
if message == "b":
if message.lower() == "b":
if bet_money == 0:
bet_money = 5
else:
bet_money = bet_money
# handle # message
if bet_money != 0:
bet_money = int(bet_money)
elif message.lower() == "r":
#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:
bet_money = int(message)
try:
bet_money = int(message)
except ValueError:
return f"\nInvalid Bet, please enter a valid number."
if bet_money <= p_chips.total or bet_money <= 1:
p_chips.bet = take_bet(bet_money, p_chips.total)
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}"
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 you can place is {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
# Save the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cash'] = p_chips.total
jackTracker[i]['cash'] = int(p_chips.total)
break
else:
# Display the statistics
@@ -299,10 +331,9 @@ def playBlackJack(nodeID, message):
msg += stats
setLastCmdJack(nodeID, "betPlaced")
if getLastCmdJack(nodeID) == "betPlaced":
setLastCmdJack(nodeID, "playing")
msg += "(H)it,(S)tand,(F)orfit,(D)ouble"
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)):
@@ -344,27 +375,29 @@ def playBlackJack(nodeID, message):
setLastCmdJack(nodeID, "dealerTurn")
else:
return "You can't Double Down, dont have enough chips"
elif choice == "resend" or choice == "r":
msg += show_some(p_hand.cards, d_cards, p_hand)
else:
return "Invalid Choice"
return "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
# Check if player bust
if player_bust(p_hand, p_chips):
d_win += 1
msg += "Player:BUST💥"
msg += f"\n💥PlayerBUST💥"
setLastCmdJack(nodeID, "dealerTurn")
if getLastCmdJack(nodeID) == "playing":
msg += stats
msg += "[H,S,F,D,L]"
msg += "[H,S,F,D]"
# Save the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cash'] = p_chips.total
jackTracker[i]['bet'] = p_chips.bet
jackTracker[i]['gameStats']['p_win'] = p_win
jackTracker[i]['gameStats']['d_win'] = d_win
jackTracker[i]['gameStats']['draw'] = draw
jackTracker[i]['cash'] = int(p_chips.total)
jackTracker[i]['bet'] = int(p_chips.bet)
jackTracker[i]['gameStats']['p_win'] = int(p_win)
jackTracker[i]['gameStats']['d_win'] = int(d_win)
jackTracker[i]['gameStats']['draw'] = int(draw)
jackTracker[i]['p_cards'] = p_cards
jackTracker[i]['d_cards'] = d_cards
jackTracker[i]['p_hand'] = p_hand
@@ -378,20 +411,22 @@ def playBlackJack(nodeID, message):
return msg
if getLastCmdJack(nodeID) == "dealerTurn":
# Dealers Turn
if not blackJack:
# recall the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
p_chips.total = jackTracker[i]['cash']
p_chips.bet = jackTracker[i]['bet']
p_win = jackTracker[i]['gameStats']['p_win']
d_win = jackTracker[i]['gameStats']['d_win']
draw = jackTracker[i]['gameStats']['draw']
p_cards = jackTracker[i]['p_cards']
d_cards = jackTracker[i]['d_cards']
p_hand = jackTracker[i]['p_hand']
d_hand = jackTracker[i]['d_hand']
next_card = jackTracker[i]['next_card']
break
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
p_chips.total = jackTracker[i]['cash']
p_chips.bet = jackTracker[i]['bet']
p_win = jackTracker[i]['gameStats']['p_win']
d_win = jackTracker[i]['gameStats']['d_win']
draw = jackTracker[i]['gameStats']['draw']
p_cards = jackTracker[i]['p_cards']
d_cards = jackTracker[i]['d_cards']
p_hand = jackTracker[i]['p_hand']
d_hand = jackTracker[i]['d_hand']
next_card = jackTracker[i]['next_card']
break
if p_hand.value <= 21:
# Dealer's Turn
@@ -400,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 += "Dealer:BUST💥"
msg += f"\n💰DealerBUST💥"
break
# Show all cards
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
@@ -408,15 +443,15 @@ def playBlackJack(nodeID, message):
# Check who wins
if push(p_hand, d_hand):
draw += 1
msg += f"👌PUSH"
msg += f"\n👌PUSH"
elif player_wins(p_hand, d_hand, p_chips):
p_win += 1
msg += f"🎉PLAYER WINS🎰"
msg += f"\n🎉PLAYER WINS🎰"
elif dealer_wins(p_hand, d_hand, p_chips):
d_win += 1
msg += f"👎DEALER WINS"
msg += f"\n👎DEALER WINS"
else:
msg += f"👎DEALER WINS"
msg += f"\n👎DEALER WINS"
# Display the Game Stats
msg += gameStats(str(p_win), str(d_win), str(draw))
@@ -424,14 +459,20 @@ def playBlackJack(nodeID, message):
# Display the chips left
if p_chips.total < 1:
if p_chips.total > 0:
msg += f"🪙Keep the change you filthy animal!"
msg += f"\n🪙Keep the change you filthy animal!"
else:
msg += "💸NO MORE MONEY! Game Over!"
msg += f"\n💸NO MORE CHIPS!🏧💳"
p_chips.total = jack_starting_cash
else:
msg += f"💰You have {p_chips.total} chips left"
# check high score
highScore = loadHSJack()
if highScore != 0 and p_chips.total > highScore['highScore']:
msg += f"\n💰HighScore💰{p_chips.total} "
saveHSJack(nodeID, p_chips.total)
else:
msg += f"\n💰You have {p_chips.total} chips "
msg += "(B)et or (L)eave table."
msg += f"\nBet or Leave?"
# Reset the game
setLastCmdJack(nodeID, "new")
@@ -443,6 +484,6 @@ def playBlackJack(nodeID, message):
jackTracker[i]['d_cards'] = []
jackTracker[i]['p_hand'] = []
jackTracker[i]['d_hand'] = []
jackTracker[i]['time'] = time.time()
jackTracker[i]['last_played'] = time.time()
return msg

View File

@@ -4,7 +4,7 @@
import random
import time
import pickle
from modules.log import *
from modules.log import logger
# Global variables
total_days = 7 # number of days or rotations the player has to play
@@ -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 = {}
@@ -110,38 +110,67 @@ def officer(nodeID):
cash = dwCashDb[i].get('cash')
# rolls to see if the officer takes drugs from you
# odds are (1 - event chance) * (officer chance) * (confiscation chance)
# currently (1 - 0.35) * (0.20) * (0.35) = 4.55%
# chance is approximate, not sure how randint handles endpoints, close enough for my purposes
if random.randint(0, 100) > 65: # confiscation chance
k = 0
j = 0
# removes all drugs from inventory tally and individual class attirbute
if random.randint(0, 100) > 65: # confiscation chance is 35%
j, k = 0, 0
for i in range(0, len(my_drugs)):
j = amount[i]
amount[i] = 0
k += j
inventory -= k
# sends 'conf' for confiscated. sending a string is better than a number here
# set the cash_taken to conf for confiscation not of cash
cash_taken = 'conf'
# Update the inventory_db
inventory -= k
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
amount = dwInventoryDb[i].get('amount')
return cash_taken
# rolls to see if the officer takes cash from you
# odds are (1 - event chance) * (officer chance) * (1 - confiscation chance)
# currently (1 - 0.35) * (0.20) * (0.65) = 8.45%
# chance is approximate, not sure how randint handles endpoints, close enough for my purposes
# rolls to see how much cash the officer takes
cash_taken = random.randint(1, cash-1)
cash -= cash_taken
# Update the cash_db and inventory_db
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb[i]['cash'] = cash
return cash_taken
def get_found_items(nodeID):
global dwInventoryDb, dwCashDb
msg = ''
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
amount = dwInventoryDb[i].get('amount')
inventory = dwInventoryDb[i].get('inventory')
amount = check_inv(nodeID)
return cash_taken
# get the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
cash = dwCashDb[i].get('cash')
if random.randint(0, 100) > 50: # 50% chance to find cash or drugs
if random.randint(0, 100) > 30: # 30% chance to find drugs
found = random.choice(range(len(my_drugs)))
# rolls to see how much of the drug the user finds
qty =random.randint(1, 80 - inventory)
amount[found] += qty
inventory += qty
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
dwInventoryDb[i]['amount'] = amount
msg = f"💊You found {qty} {my_drugs[found].name}"
else:
# rolls to see how much cash the user finds
cash_found = random.randint(1, 977)
cash += cash_found
# Update the cash_db
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb[i]['cash'] = cash
msg = "You found $" + str(cash_found) + "💸"
return msg
def price_change(event_number):
@@ -203,14 +232,17 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1):
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
msg += " The going price is: $" + str(price_list[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(cost) + " "
buy_amount = value
if buy_amount == 'm':
buy_amount = cash // price_list[drug_choice]
if buy_amount > 100 - inventory:
buy_amount = 100 - inventory
if buy_amount == 0:
return "You don\'t have any empty inventory slots.🎒"
# set the buy amount to the max if the user enters m
buy_amount = int(buy_amount)
@@ -218,7 +250,7 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
msg = f"Didnt see a qty. ex: b,1,10 buys 10 of {my_drugs[1].name}, can also use m for max"
return msg
elif buy_amount not in range(1, 101):
msg = f"Enter qty or m for max"
msg = "Enter qty or m for max"
return msg
elif buy_amount > 100 - inventory:
msg = "You don\'t have enough space for all that.🎒"
@@ -228,7 +260,7 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
cash -= buy_amount * price_list[drug_choice]
inventory += buy_amount
msg += "You bought " + str(buy_amount) + " " + my_drugs[drug_choice].name + '. Remaining cash: $' + str(cash)
msg += f"\nBuy Sell Fly?"
msg += f"\nBuy💸, Sell💰, Fly🛫?"
else:
msg = "You don't have enough cash!😭"
return msg
@@ -273,10 +305,10 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
else:
sell_amount = int(sell_amount)
if sell_amount not in range(1, 101):
msg = f"You can only sell between 1 and 100"
msg = "You can only sell between 1 and 100"
return msg
except ValueError:
msg = f"Enter qty or m for max"
msg = "Enter qty or m for max"
return msg
# check if the user has any of the drug they are trying to sell
@@ -286,15 +318,17 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
" The going price is: $" + str(price_list[drug_choice])
" The going price is: $" + str("{:,}".format(cost))
# check if the user has enough of the drug to sell
if sell_amount <= amount[drug_choice]:
amount[drug_choice] -= sell_amount
cash += sell_amount * price_list[drug_choice]
inventory -= sell_amount
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name + ' for $' +\
str(sell_amount * price_list[drug_choice]) + '. Total cash: $' + str(cash)
profit = sell_amount * price_list[drug_choice]
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name +\
' for $' + "{:,}".format(profit) + '. Total cash: $' + "{:,}".format(cash)
else:
msg = "You don't have that much"
return msg
@@ -317,7 +351,6 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
return msg
def get_location_table(nodeID, choice=0):
global dwLocationDb
# get the location for the user
@@ -329,12 +362,12 @@ def get_location_table(nodeID, choice=0):
loc_table_string = ''
for i in range(len(loc)):
loc_table_string += str(i+1) + '. ' + loc[i] + ' '
loc_table_string += f' Where do you want to 🛫?#'
loc_table_string += ' Where do you want to 🛫?#'
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
@@ -343,29 +376,12 @@ 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)})
with open('dopewar_hs.pkl', 'wb') as file:
with open('data/dopewar_hs.pkl', 'wb') as file:
pickle.dump(dwHighScore, file)
msg = "You finished with $" + str(cash) + " and beat the high score!🎉💰"
msg = "You finished with $" + "{:,}".format(cash) + " and beat the high score!🎉💰"
return msg
if cash > starting_cash:
msg = 'You made money! 💵 Up ' + str((cash/starting_cash).__round__()) + 'x! Well done.'
@@ -375,8 +391,6 @@ def endGameDw(nodeID):
return msg
if cash < starting_cash:
msg = "You lost money, better go get a real job.💸"
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
return msg
@@ -384,31 +398,35 @@ def getHighScoreDw():
global dwHighScore
# Load high score table
try:
with open('dopewar_hs.pkl', 'rb') as file:
with open('data/dopewar_hs.pkl', 'rb') as file:
dwHighScore = pickle.load(file)
except FileNotFoundError:
logger.debug("System: DopeWars: No high score table found")
# high score pickle file is a touple of the nodeID and the high score
dwHighScore = ({"userID": 4258675309, "cash": 100})
# write a new high score file if one is not found
with open('dopewar_hs.pkl', 'wb') as file:
with open('data/dopewar_hs.pkl', 'wb') as file:
pickle.dump(dwHighScore, file)
return dwHighScore
def render_game_screen(userID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen):
def render_game_screen(userID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen, found_items):
global dwCashDb, dwInventoryDb, dwLocationDb
msg = ''
# get the location for the user
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == userID:
loc = dwLocationDb[i].get('location')
if event_number != -1:
msg += event_list[event_number].text + f"\n"
if event_number == -1 and cash_stolen != 0 and cash_stolen != 'conf':
msg += "🚔Officer Leroy stopped you and took $" + str(cash_stolen) + "💸" + f"\n"
if event_number == -1 and cash_stolen == 'conf':
msg += "🚔Officer Leroy stopped you and took all of your drugs.🚭" + f"\n"
elif event_number == -1 and cash_stolen != 0 and cash_stolen != 'conf':
msg += random.choice([f"You got high and spent ${str(cash_stolen)}💊💸\n",
f"You got mugged and lost ${str(cash_stolen)}💸🔫\n",
f"You got a new tattoo and spent ${str(cash_stolen)}💉💸\n",])
elif event_number == -1 and cash_stolen == 'conf':
msg += f"🚔Officer Bob stopped you and took all of your drugs.🚭\n"
elif event_number == -1 and found_items != 'nothing':
msg += found_items + f"\n"
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
@@ -421,18 +439,18 @@ def render_game_screen(userID, day_play, total_day, loc_choice, event_number, pr
if dwCashDb[i].get('userID') == userID:
cash = dwCashDb[i].get('cash')
msg += "Location: " + loc[int(loc_choice) - 1] + ", Day:" + str(day_play) + '/' + str(total_day) + " 🎒: " + str(inventory) + "/100" + ", $" + str(cash) + f"\n"
msg += "🗺️" + loc[int(loc_choice) - 1] + " 📆" + str(day_play) + '/' + str(total_day) + " 🎒" + str(inventory) + "/100" + " 💵" + "{:,}".format(cash) + f"\n"
for i, drug in enumerate(my_drugs, 1):
qty = amount[i-1]
msg += f'#{str(i)}.{drug.name}/${price_list[i-1]}({qty}) '
msg += f'#{str(i)}.{drug.name}${"{:,}".format(price_list[i-1])}({qty}) '
return msg
def dopeWarGameDay(nodeID, day_play, total_day):
global dwCashDb, dwLocationDb, dwInventoryDb
cash_stolen = 0
found_items = 'nothing'
# roll for the event of the day
event_number = generate_event()
@@ -443,12 +461,14 @@ def dopeWarGameDay(nodeID, day_play, total_day):
loc = dwLocationDb[i].get('location')
loc_choice = dwLocationDb[i].get('loc_choice')
# rolls to see if the officer event happens
# odds are (1 - event chance) * (officer chance)
# currently (1 - 0.35) * (0.20) = 13%
# chance is approximate, not sure how randint handles endpoints, close enough for my purposes
if event_number == -1 and random.randint(0, 100) > 80:
cash_stolen = officer(nodeID)
# rolls to see if event happens
if event_number == -1 and random.randint(0, 100) > 80: # 20% chance to have an event
if random.randint(0, 100) > 50: # 50% chance to have an officer encounter
cash_stolen = officer(nodeID)
else:
# find items
found_items = get_found_items(nodeID)
price_list = price_change(event_number)
@@ -460,7 +480,7 @@ def dopeWarGameDay(nodeID, day_play, total_day):
check_inv(nodeID)
# main game display print
msg = render_game_screen(nodeID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen)
msg = render_game_screen(nodeID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen, found_items)
return msg
@@ -516,7 +536,7 @@ def playDopeWars(nodeID, cmd):
dwPlayerTracker[i]['cmd'] = 'location'
if last_cmd == 'ask_bsf':
msg = 'example Buy: b,Drug,Qty or Sell s,1,10. Fly: f. Price list: p or end'
msg = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
menu_choice = cmd.lower()
if ',' in menu_choice or '.' in menu_choice:
#split the choice into a letter and a number for the buy/sell functions
@@ -539,7 +559,7 @@ def playDopeWars(nodeID, cmd):
menu_choice[2] = int(menu_choice[2])
except ValueError:
msg = 'a value was bad, example dopeware Buy or Sell b,1,10 or s,1,m'
msg = f'a value was bad, example dopeware Buy or Sell\n b,1,10 or s,1,m'
return msg
if menu_choice[0] == 'b':
@@ -570,9 +590,9 @@ def playDopeWars(nodeID, cmd):
sell = sell_func(nodeID, price_list, i, 'm')
# ignore starts with "You don't have any"
if not sell.startswith("You don't have any"):
msg += sell
if i != len(my_drugs):
msg += '\n'
msg += sell + '\n'
# trim the last newline
msg = msg[:-1]
return msg
elif 'f' in menu_choice:
# set last command to location
@@ -583,13 +603,13 @@ def playDopeWars(nodeID, cmd):
elif 'p' in menu_choice:
# render_game_screen
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0)
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0, 'nothing')
return msg
elif 'end' in menu_choice:
elif 'e' in menu_choice:
msg = endGameDw(nodeID)
return msg
else:
msg = 'example Buy: b,Drug,Qty or Sell s,1,10. Fly: f. Price list: p or end'
msg = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
return msg
# Buy
@@ -639,11 +659,12 @@ def playDopeWars(nodeID, cmd):
# Display Main Game Screen and ask for buy, sell, or fly
if last_cmd == 'display_main':
msg = dopeWarGameDay(nodeID, game_day, total_days)
msg += f"\nBuy, Sell, Fly? Price list?"
msg += f"\nBuy💸, Sell💰, (F)ly🛫? (P)riceList?"
# set the player's last command
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
dwPlayerTracker[i]['last_played'] = time.time()
# Game end
if game_day == total_days + 1:

415
modules/games/golfsim.py Normal file
View File

@@ -0,0 +1,415 @@
# https://github.com/danfriedman30/pythongame
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
import pickle
from modules.log import logger
# Clubs setup
driver_distances = list(range(230, 280, 5))
low_distances = list(range(185, 215, 5))
mid_distances = list(range(130, 185, 5))
high_distances = list(range(90, 135, 5))
gap_wedge_distances = list(range(50, 85, 5))
lob_wedge_distances = list(range(10, 50, 5))
putt_outcomes = [1, 2, 3]
# Hole/Course Setup
full_hole_range = list(range(130, 520, 5))
par3_range = list(range(130, 255, 5))
par4_range = list(range(255, 445, 5))
par5_range = list(range(445, 520, 5))
par3_4_range = par3_range + par4_range
par3_5_range = par3_range + par5_range
par4_5_range = par4_range + par5_range
# Player setup
playingHole = False
from modules.settings import golfTracker
# Club functions
def hit_driver():
club_distance = random.choice(driver_distances)
return club_distance
def hit_low_iron():
club_distance = random.choice(low_distances)
return club_distance
def hit_mid_iron():
club_distance = random.choice(mid_distances)
return club_distance
def hit_high_iron():
club_distance = random.choice(high_distances)
return club_distance
def hit_gap_wedge():
club_distance = random.choice(gap_wedge_distances)
return club_distance
def hit_lob_wedge():
club_distance = random.choice(lob_wedge_distances)
return club_distance
def finish_hole():
finish = random.choice(putt_outcomes)
return finish
def endGameGolf(nodeID):
# pop player from tracker
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker.pop(i)
logger.debug("System: GolfSim: Player " + str(nodeID) + " has ended their round.")
def getScorecardGolf(scorecard):
# Scorecard messages, convert score to message comment
msg = ""
if scorecard == 8:
# Quadruple bogey
msg += " +Quad Bogey☃ "
elif scorecard == 7:
# Triple bogey
msg += " +Triple Bogey "
elif scorecard == 6:
# Double bogey
msg += " +Double Bogey "
elif scorecard == 5:
# Bogey
msg += " +Bogey "
elif scorecard > 0:
# Over par
msg += f" +Par {str(scorecard)} "
elif scorecard == 0:
# Even par
msg += " Even Par💪 "
elif scorecard == -1:
# Birdie
msg += " -Birdie🐦 "
elif scorecard == -2:
# Eagle
msg += " -Eagle🦅 "
elif scorecard == -3:
# Albatross
msg += " -Albatross🦅🦅 "
else:
# Under par
msg += f" -Par {str(abs(scorecard))} "
return msg
def getHighScoreGolf(nodeID, strokes, par):
# check if player is in high score list
try:
with open('data/golfsim_hs.pkl', 'rb') as f:
golfHighScore = pickle.load(f)
except:
logger.debug("System: GolfSim: High Score file not found.")
golfHighScore = [{'nodeID': nodeID, 'strokes': strokes, 'par': par}]
with open('data/golfsim_hs.pkl', 'wb') as f:
pickle.dump(golfHighScore, f)
if strokes < golfHighScore[0]['strokes']:
# player got new low score which is high score
golfHighScore[0]['nodeID'] = nodeID
golfHighScore[0]['strokes'] = strokes
golfHighScore[0]['par'] = par
with open('data/golfsim_hs.pkl', 'wb') as f:
pickle.dump(golfHighScore, f)
return golfHighScore
return 0
# Main game loop
def playGolf(nodeID, message, finishedHole=False, last_cmd=''):
msg = ''
# Course setup
par3_count = 0
par4_count = 0
par5_count = 0
# Scorecard setup
total_strokes = 0
total_to_par = 0
par = 0
hole = 1
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
last_cmd = golfTracker[i]['cmd']
hole = golfTracker[i]['hole']
distance_remaining = golfTracker[i]['distance_remaining']
hole_shots = golfTracker[i]['hole_shots']
par = golfTracker[i]['par']
total_strokes = golfTracker[i]['total_strokes']
total_to_par = golfTracker[i]['total_to_par']
#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
if par3_count < 2 and par4_count < 5 and par5_count < 2:
hole_length = random.choice(full_hole_range)
if par3_count >= 2 and par4_count < 5 and par5_count < 2:
hole_length = random.choice(par4_5_range)
if par3_count >= 2 and par4_count < 5 and par5_count >= 2:
hole_length = random.choice(par4_range)
if par3_count < 2 and par4_count < 5 and par5_count >= 2:
hole_length = random.choice(par3_4_range)
if par3_count < 2 and par4_count >= 5 and par5_count >= 2:
hole_length = random.choice(par3_range)
if par3_count >= 2 and par4_count >= 5 and par5_count < 2:
hole_length = random.choice(par5_range)
if par3_count < 2 and par4_count >= 5 and par5_count < 2:
hole_length = random.choice(par3_5_range)
# Set up par for the hole
if hole_length <= 250:
par = 3
par3_count += 1
elif hole_length > 250 and hole_length <= 440:
par = 4
par4_count += 1
elif hole_length > 440:
par = 5
par5_count += 1
# roll for chance of hazard
hazard_chance = random.randint(1, 100)
weather_chance = random.randint(1, 100)
# have low chances of hazards and weather
hasHazard = False
hazard = ""
if hazard_chance < 25:
# Further reduce chance of hazards with weather
if weather_chance < 15:
# randomly calculate a hazard for the hole sand, 🌊, 🌲, 🏘️, etc
hazard = random.choice(["🏖️", "🌊", "🌲", "🏘️"])
hasHazard = True
# 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]['last_played'] = time.time()
golfTracker[i]['hole_shots'] = hole_shots
# Show player the hole information
msg += "⛳️#" + str(hole) + " is a " + str(hole_length) + "-yard Par " + str(par) + "."
if hasHazard:
msg += "⚠️" + hazard + "."
else:
# add weather conditions with random choice from list, this is fluff
msg += random.choice(["☀️", "💨", "☀️", "☀️", "⛅️", "☁️", "☀️"])
if not finishedHole:
msg += f"\nChoose your club."
return msg
if last_cmd == 'stroking':
# Get player's current game state
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
distance_remaining = golfTracker[i]['distance_remaining']
hole = golfTracker[i]['hole']
hole_shots = golfTracker[i]['hole_shots']
par = golfTracker[i]['par']
total_strokes = golfTracker[i]['total_strokes']
total_to_par = golfTracker[i]['total_to_par']
hazard = golfTracker[i]['hazard']
# Start loop to be able to choose clubs while at least 20 yards away
if distance_remaining >= 20:
msg = ""
club = message.lower()
shot_distance = 0
pin_distance = distance_remaining
if club == "driver" or club.startswith("d"):
shot_distance = hit_driver()
msg += "🏌Hit D " + str(shot_distance) + "yd. "
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif "low" in club or club.startswith("l"):
shot_distance = hit_low_iron()
msg += "🏌Hit L Iron " + str(shot_distance) + "yd. "
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif "mid" in club or club.startswith("m"):
shot_distance = hit_mid_iron()
msg += "🏌Hit M Iron " + str(shot_distance) + "yd. "
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif "high" in club or club.startswith("h"):
shot_distance = hit_high_iron()
msg += "🏌Hit H Iron " + str(shot_distance) + "yd. "
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif "gap" in club or club.startswith("g"):
shot_distance = hit_gap_wedge()
msg += "🏌Hit G Wedge " + str(shot_distance) + "yd ."
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif "wedge" in club or club.startswith("w"):
shot_distance = hit_lob_wedge()
msg += "🏌Hit L Wedge " + str(shot_distance) + "yd. "
distance_remaining = abs(distance_remaining - shot_distance)
hole_shots += 1
elif club == "caddy" or club.startswith("c"):
# Show player the club distances
msg += f"Caddy Guess:\nD:{hit_driver()} L:{hit_low_iron()} M:{hit_mid_iron()} H:{hit_high_iron()} G:{hit_gap_wedge()} W:{hit_lob_wedge()}"
else:
msg += f"Didnt get your club 🥪♣️🪩 choice, you have {distance_remaining}yds. to ⛳️"
return msg
if distance_remaining - pin_distance > pin_distance or shot_distance > pin_distance:
# Check for over-shooting the hole
if distance_remaining > 20:
# did it go off the "green"?
msg += "Overshot the green!🚀"
if distance_remaining == 0:
msg += "🎯Perfect shot! "
last_cmd = 'putt'
elif distance_remaining < 20:
# Roll Dice
hole_in_one_chance = random.randint(1, 100)
wind_factor = random.randint(1, 10)
skill_factor = random.randint(1, 10)
critter_factor = random.randint(1, 50)
# Check for hole in one
if hole_in_one_chance <= 5 and wind_factor > 7 and skill_factor > 8:
distance_remaining = 0
# Check for critters
if skill_factor > 8 and critter_factor < 40 and wind_factor > 2 and hole_in_one_chance > 5:
msg += random.choice(["A 🐿️ steals your ball!😡 ","You Hit a 🦅 soring past ", "🐊 need we say more? ", "hit a 🪟 of a 🏡 "])
distance_remaining = -1
# Handle hazard
if hazard == "🌊" and skill_factor < 7:
msg += "In the water!🌊"
distance_remaining = -1
if hazard == "🏖️" and skill_factor < 5:
msg += "In the sand!🏖️"
distance_remaining = random.randint(5, 10)
if hazard == "🌲" and skill_factor < 3:
msg += "In the trees!🌲"
distance_remaining += random.randint(5, 20)
if hazard == "🏘️" and skill_factor < 2:
msg += "In the parking lot!🚗"
distance_remaining += random.randint(10, 30)
# Check we didnt go off the green or into a hazard
if distance_remaining < 20:
last_cmd = 'putt'
else:
last_cmd = 'stroking'
else:
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
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['distance_remaining'] = distance_remaining
golfTracker[i]['hole_shots'] = hole_shots
golfTracker[i]['total_strokes'] = total_strokes
golfTracker[i]['cmd'] = 'stroking'
return msg
if last_cmd == 'putt':
# Finish the hole by putting
critter = False
if distance_remaining < 20:
if distance_remaining == 0:
putts = 0
elif distance_remaining == -1:
putts = 0
critter = True
else:
putts = finish_hole()
# Calculate hole and round scores
hole_strokes = hole_shots + putts
hole_to_par = hole_strokes - par
total_strokes += hole_strokes
total_to_par += hole_to_par
if not critter:
# Show player hole/round scoring info
if putts == 0 and hole_strokes == 1:
msg += "🎯Hole in one!⛳️"
elif putts == 0:
msg += "You're in the hole at " + str(hole_strokes) + " strokes!"
else:
msg += "You're on the green! After " + str(putts) + " putt(s), you're in for " + str(hole_strokes) + " strokes."
msg += getScorecardGolf(hole_to_par)
if hole not in [1, 10]:
# Show player total scoring info for the round, except hole 1 and 10
msg += f"\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
msg += getScorecardGolf(total_to_par)
# Move to next hole
hole += 1
else:
msg += f"Got a new ball at Pro-Shop, marshal put you @" # flow into same hole haha
# Scorecard reset
hole_to_par = 0
hole_strokes = 0
hole_shots = 0
# Save player's current game state
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['hole_strokes'] = hole_strokes
golfTracker[i]['hole_to_par'] = hole_to_par
golfTracker[i]['total_strokes'] = total_strokes
golfTracker[i]['total_to_par'] = total_to_par
golfTracker[i]['hole'] = hole
golfTracker[i]['cmd'] = 'new'
golfTracker[i]['last_played'] = time.time()
if hole >= 9:
# Final score messages & exit prompt
msg += f"🎉Finished 9-hole round⛳"
#HighScore Display
highscore = getHighScoreGolf(nodeID, total_strokes, total_to_par)
if highscore != 0:
msg += " 🏆New Club Record🏆"
# pop player from tracker
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker.pop(i)
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
else:
# Show player the next hole
msg += playGolf(nodeID, '', True, last_cmd='new')
msg += f"\n🏌️[D, L, M, H, G, W, End]🏌️"
return msg

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

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

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

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

193
modules/games/joke.py Normal file
View File

@@ -0,0 +1,193 @@
# This module is used to tell jokes to the user
# The emoji table of contents is used to replace words in the joke with emojis
# As a Ham, is this obsecuring the meaning of the joke? Or is it enhancing it?
from dadjokes import Dadjoke # pip install dadjokes
import random
from modules.log import logger, getPrettyTime
from modules.settings import dad_jokes_emojiJokes, dad_jokes_enabled
lameJokes = [
"Why don't scientists trust atoms? Because they make up everything!",
"Why did the scarecrow win an award? Because he was outstanding in his field!",
"Why don't skeletons fight each other? They don't have the guts.",
"What do you call fake spaghetti? An impasta!",
"Why did the bicycle fall over? Because it was two-tired!",
"Why did the math book look sad? Because it had too many problems.",
"Why did the golfer bring two pairs of pants? In case he got a hole in one.",
"Why did the coffee file a police report? It got mugged.",
"Why did the tomato turn red? Because it saw the salad dressing!",
"Why did the cookie go to the doctor? Because it felt crummy.",
"Why did the computer go to the doctor? Because it had a virus!",
"Why did the chicken join a band? Because it had the drumsticks!",
"Why did the banana go to the doctor? Because it wasn't peeling well.",
"Why did the cow go to space? To see the moooon!",
"Why did the fish blush? Because it saw the ocean's bottom!",
"Why did the elephant bring a suitcase to the zoo? Because it wanted to pack its trunk!",
"Why did the meshtastic node go to therapy? It had too many connections to handle!",
"Why did the meshtastic user bring a ladder to the meeting? To reach new heights in communication!",
"Why did the meshtastic device break up with Wi-Fi? It found a better connection!",
"Why did the meshtastic network throw a party? Because it wanted to mesh well with everyone!",
"Why did the meshtastic node get promoted? Because it was outstanding in its field!",
"Why did the meshtastic user bring a map? To navigate the mesh of possibilities!",
"Why did the meshtastic device go to school? To improve its signal strength!",
"How did the meshtastic node become a comedian? It uses mesh-bots to deliver punchlines!",
"Chuck Norris doesn't read books. He stares them down until he gets the information he wants.",
"When Chuck Norris enters a room, he doesn't turn the lights on. He turns the dark off.",
"Chuck Norris can divide by zero.",
"Chuck Norris counted to infinity. Twice.",
"Chuck Norris can slam a revolving door.",
"When Chuck Norris does a push-up, he isn't lifting himself up; he's pushing the Earth down.",
"Chuck Norris can hear sign language.",
"Death once had a near-Chuck Norris experience.",
"Chuck Norris can unscramble an egg.",
"Chuck Norris can win a game of Connect Four in only three moves.",
"Chuck Norris can make a snowman out of rain.",
"Chuck Norris can strangle you with a cordless phone.",
"Chuck Norris can do a wheelie on a unicycle.",
"Chuck Norris can kill two stones with one bird.",
"Chuck Norris can speak braille.",
"Chuck Norris can build a snowman out of rain.",
"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():
wordToEmojiMap = {
'love': '❤️', 'heart': '❤️', 'happy': '😊', 'smile': '😊', 'sad': '😢', 'angry': '😠', 'mad': '😠', 'cry': '😢', 'laugh': '😂', 'funny': '😂', 'cool': '😎',
'joy': '😂', 'kiss': '😘', 'hug': '🤗', 'wink': '😉', 'grin': '😁', 'bored': '😐', 'tired': '😴', 'sleepy': '😴', 'shocked': '😲', 'surprised': '😲',
'confused': '😕', 'thinking': '🤔', 'sick': '🤢', 'party': '🎉', 'celebrate': '🎉', 'clap': '👏', 'thumbs up': '👍', 'thumbs down': '👎',
'ok': '👌', 'wave': '👋', 'pray': '🙏', 'muscle': '💪', 'fire': '🔥', 'star': '', 'sun': '☀️', 'moon': '🌙', 'rain': '🌧️', 'snow': '❄️', 'cloud': '☁️',
'dog': '🐶', 'cat': '🐱', 'mouse': '🐭', 'rabbit': '🐰', 'fox': '🦊', 'bear': '🐻', 'panda': '🐼', 'koala': '🐨', 'tiger': '🐯', 'lion': '🦁', 'cow': '🐮',
'pig': '🐷', 'frog': '🐸', 'monkey': '🐵', 'chicken': '🐔', 'penguin': '🐧', 'bird': '🐦', 'fish': '🐟', 'whale': '🐋', 'dolphin': '🐬', 'octopus': '🐙',
'apple': '🍎', 'orange': '🍊', 'banana': '🍌', 'watermelon': '🍉', 'grape': '🍇', 'strawberry': '🍓', 'cherry': '🍒', 'peach': '🍑', 'pineapple': '🍍', 'mango': '🥭', 'coconut': '🥥',
'tomato': '🍅', 'eggplant': '🍆', 'avocado': '🥑', 'broccoli': '🥦', 'cucumber': '🥒', 'corn': '🌽', 'carrot': '🥕', 'potato': '🥔', 'sweet potato': '🍠', 'chili': '🌶️', 'garlic': '🧄',
'pizza': '🍕', 'burger': '🍔', 'fries': '🍟', 'hotdog': '🌭', 'popcorn': '🍿', 'donut': '🍩', 'cookie': '🍪', 'cake': '🎂', 'pie': '🍰', 'cupcake': '🧁', 'chocolate': '🍫',
'candy': '🍬', 'lollipop': '🍭', 'pudding': '🍮', 'honey': '🍯', 'milk': '🍼', 'coffee': '', 'tea': '🍵', 'sake': '🍶', 'beer': '🍺', 'cheers': '🍻', 'champagne': '🥂',
'wine': '🍷', 'whiskey': '🥃', 'cocktail': '🍸', 'tropical drink': '🍹', 'bottle': '🍾', 'soda': '🥤', 'chopsticks': '🥢', 'fork': '🍴', 'knife': '🔪', 'spoon': '🥄', 'kitchen knife': '🔪',
'house': '🏠', 'home': '🏡', 'office': '🏢', 'post office': '🏣', 'hospital': '🏥', 'bank': '🏦', 'hotel': '🏨', 'love hotel': '🏩', 'convenience store': '🏪', 'school': '🏫', 'department store': '🏬',
'factory': '🏭', 'castle': '🏯', 'palace': '🏰', 'church': '💒', 'tower': '🗼', 'statue of liberty': '🗽', 'mosque': '🕌', 'synagogue': '🕍', 'hindu temple': '🛕', 'kaaba': '🕋', 'shinto shrine': '⛩️',
'railway': '🛤️', 'highway': '🛣️', 'map': '🗾', 'carousel': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'circus': '🎪', 'theater': '🎭', 'art': '🎨', 'slot machine': '🎰', 'dice': '🎲',
'bowling': '🎳', 'video game': '🎮', 'dart': '🎯', 'billiard': '🎱', 'medal': '🎖️', 'trophy': '🏆', 'gold medal': '🥇', 'silver medal': '🥈', 'bronze medal': '🥉', 'soccer': '', 'baseball': '',
'basketball': '🏀', 'volleyball': '🏐', 'football': '🏈', 'rugby': '🏉', 'tennis': '🎾', 'frisbee': '🥏', 'ping pong': '🏓', 'badminton': '🏸', 'boxing': '🥊', 'martial arts': '🥋',
'goal': '🥅', 'golf': '', 'skating': '⛸️', 'fishing': '🎣', 'diving': '🤿', 'running': '🎽', 'skiing': '🎿', 'sledding': '🛷', 'curling': '🥌', 'climbing': '🧗', 'yoga': '🧘',
'surfing': '🏄', 'swimming': '🏊', 'water polo': '🤽', 'cycling': '🚴', 'mountain biking': '🚵', 'horse riding': '🏇', 'kneeling': '🧎', 'weightlifting': '🏋️', 'gymnastics': '🤸', 'wrestling': '🤼', 'handball': '🤾',
'juggling': '🤹', 'meditation': '🧘', 'sauna': '🧖', 'rock climbing': '🧗', 'stop': '🛑', 'computer': '💻', 'phone': '📱', 'email': '📧', 'camera': '📷', 'video': '📹', 'music': '🎵',
'guitar': '🎸', 'piano': '🎹', 'drum': '🥁', 'microphone': '🎤', 'headphone': '🎧', 'book': '📚', 'newspaper': '📰', 'magazine': '📖', 'pen': '🖊️', 'pencil': '✏️', 'paintbrush': '🖌️',
'scissors': '✂️', 'ruler': '📏', 'globe': '🌍', 'earth': '🌎', 'star': '🌟', 'comet': '☄️', 'rocket': '🚀', 'airplane': '✈️', 'car': '🚗', 'bus': '🚌', 'train': '🚆',
'bicycle': '🚲', 'motorcycle': '🏍️', 'boat': '🚤', 'ship': '🚢', 'helicopter': '🚁', 'tractor': '🚜', 'ambulance': '🚑', 'fire truck': '🚒', 'police car': '🚓', 'taxi': '🚕', 'truck': '🚚',
'construction': '🚧', 'traffic light': '🚦', 'stop sign': '🛑', 'fuel': '', 'battery': '🔋', 'light bulb': '💡', 'flashlight': '🔦', 'candle': '🕯️', 'lamp': '🛋️',
'bed': '🛏️', 'sofa': '🛋️', 'chair': '🪑', 'table': '🛋️', 'toilet': '🚽', 'shower': '🚿', 'bathtub': '🛁', 'sink': '🚰', 'mirror': '🪞', 'door': '🚪', 'window': '🪟',
'key': '🔑', 'lock': '🔒', 'hammer': '🔨', 'wrench': '🔧', 'screwdriver': '🪛', 'saw': '🪚', 'drill': '🛠️', 'toolbox': '🧰', 'paint roller': '🖌️', 'brush': '🖌️', 'broom': '🧹',
'mop': '🧽', 'bucket': '🪣', 'vacuum': '🧹', 'washing machine': '🧺', 'dryer': '🧺', 'iron': '🧺', 'hanger': '🧺', 'laundry': '🧺', 'basket': '🧺', 'trash': '🗑️', 'recycle': '♻️',
'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': '🍍',
'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': '🥗',
'popcorn': '🍿', 'butter': '🧈', 'salt': '🧂', 'canned food': '🥫', 'bento box': '🍱', 'rice cracker': '🍘', 'rice ball': '🍙', 'cooked rice': '🍚', 'curry rice': '🍛', 'steaming bowl': '🍜',
'spaghetti': '🍝', 'roasted sweet potato': '🍠', 'oden': '🍢', 'sushi': '🍣', 'fried shrimp': '🍤', 'fish cake': '🍥', 'moon cake': '🥮', 'dango': '🍡', 'dumpling': '🥟', 'fortune cookie': '🥠',
'takeout box': '🥡', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'oyster': '🦪', 'ice cream': '🍨', 'shaved ice': '🍧', 'ice cream cone': '🍦', 'doughnut': '🍩', 'cookie': '🍪',
'birthday cake': '🎂', 'shortcake': '🍰', 'cupcake': '🧁', 'pie': '🥧', 'chocolate bar': '🍫', 'candy': '🍬', 'lollipop': '🍭', 'custard': '🍮', 'honey pot': '🍯', 'baby bottle': '🍼',
'glass of milk': '🥛', 'hot beverage': '', 'teapot': '🫖', 'teacup without handle': '🍵', 'sake': '🍶', 'bottle with popping cork': '🍾', 'wine glass': '🍷', 'cocktail glass': '🍸', 'tropical drink': '🍹',
'beer mug': '🍺', 'clinking beer mugs': '🍻', 'clinking glasses': '🥂', 'tumbler glass': '🥃', 'cup with straw': '🥤', 'bubble tea': '🧋', 'beverage box': '🧃', 'mate': '🧉', 'ice': '🧊',
'chopsticks': '🥢', 'fork and knife': '🍴', 'spoon': '🥄', 'kitchen knife': '🔪', 'amphora': '🏺', 'globe showing Europe-Africa': '🌍', 'globe showing Americas': '🌎', 'globe showing Asia-Australia': '🌏',
'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': '🏪',
'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': '🔭', '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': '🔊', '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': '🦿',
'prosthetic arm': '🦾', 'prosthetic leg': '🦿', 'wheelchair': '🦽', 'crutch': '🦯', 'hearing aid': '🦻', 'glasses': '👓', 'magnifying glass': '🔍', 'circus tent': '🎪',
'duck': '🦆', 'eagle': '🦅', 'owl': '🦉', 'bat': '🦇', 'shark': '🦈', 'butterfly': '🦋', 'snail': '🐌', 'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗',
'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': '🐫',
'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': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
return wordToEmojiMap
def sendWithEmoji(message):
# this will take a string of text and replace any word or phrase that is in the word list with the corresponding emoji
wordToEmojiMap = tableOfContents()
# type format to clean it up
words = message.split()
i = 0
while i < len(words):
for phrase in sorted(wordToEmojiMap.keys(), key=len, reverse=True):
phrase_words = phrase.split()
# Strip punctuation from the words
stripped_words = [word.lower().strip('.,!?') for word in words[i:i+len(phrase_words)]]
if stripped_words == phrase_words:
logger.debug(f"System: Replaced the phrase '{phrase}' with '{wordToEmojiMap[phrase]}'")
words[i:i+len(phrase_words)] = [wordToEmojiMap[phrase]]
i += len(phrase_words) - 1
break
# Check for plural forms
elif stripped_words == [word + 's' for word in phrase_words]:
logger.debug(f"System: Replaced the plural phrase '{' '.join([word + 's' for word in phrase_words])}' with '{wordToEmojiMap[phrase]}'")
words[i:i+len(phrase_words)] = [wordToEmojiMap[phrase]]
i += len(phrase_words) - 1
break
i += 1
return ' '.join(words)
def tell_joke(nodeID=0, vox=False, test=False):
dadjoke = Dadjoke()
if test:
return sendWithEmoji(dadjoke.joke)
try:
if dad_jokes_emojiJokes:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke
return renderedLaugh
except Exception as e:
return random.choice(lameJokes)

View File

@@ -6,8 +6,8 @@ from random import randrange, uniform # random numbers
from types import SimpleNamespace # namespaces support
import pickle # pickle file support
import time # time functions
from modules.log import * # mesh-bot logging
from modules.log import logger # mesh-bot logging
from modules.system import lemonadeTracker # player tracking
import locale # culture specific locale
import math # math functions
import re # regular expressions
@@ -18,11 +18,10 @@ locale.setlocale(locale.LC_ALL, '')
lemon_starting_cash = 30.00
lemon_total_weeks = 7
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'time': time.time()}]
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00}]
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}]
def get_sales_amount(potential, unit, price):
@@ -38,29 +37,30 @@ def get_sales_amount(potential, unit, price):
return math.floor(potential * (unit / (price ** 1.5)))
def getHighScoreLemon():
global high_score
high_score = {"userID": 0, "cash": 0, "success": 0}
# Load high score table
try:
with open('lemonade_hs.pkl', 'rb') as file:
with open('data/lemonstand.pkl', 'rb') as file:
high_score = pickle.load(file)
except FileNotFoundError:
logger.debug("System: Lemonade: No high score table found")
# high score pickle file is a touple of the nodeID and the high score
high_score = ({"userID": 4258675309, "cash": 2, "success": 0})
# write a new high score file if one is not found
with open('lemonade_hs.pkl', 'wb') as file:
with open('data/lemonstand.pkl', 'wb') as file:
pickle.dump(high_score, file)
return high_score
def start_lemonade(nodeID, message, celsius=False):
def playLemonstand(nodeID, message, celsius=False, newgame=False):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
msg = ""
potential = 0
unit = 0.0
price = 0.0
total_sales = 0
lemonsLastCmd = ''
high_score = getHighScoreLemon()
def saveValues():
def saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score):
# save playerDB values
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
@@ -89,39 +89,13 @@ def start_lemonade(nodeID, message, celsius=False):
lemonadeWeeks[i]['potential'] = potential
lemonadeWeeks[i]['unit'] = unit
lemonadeWeeks[i]['price'] = price
lemonadeWeeks[i]['total_sales'] = weeks.total_sales
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
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 "end" in message.lower():
endGame(nodeID)
return "Goodbye!👋"
title="Lemonade Stand🍋"
title="LemonStand🍋"
# Define the temperature unit symbols
fahrenheit_unit = "ºF"
celsius_unit = "ºC"
@@ -171,6 +145,7 @@ def start_lemonade(nodeID, message, celsius=False):
'current' : 1, # start with the 1st week
'total' : 12, # span the 12 weeks of Summer
'sales' : 99, # 99 maximum sales per week
'total_sales' : 0, # total sales
'summary' : [] # empty array
}
weeks = SimpleNamespace(**weeksd)
@@ -212,7 +187,7 @@ def start_lemonade(nodeID, message, celsius=False):
inventory.sugar = lemonadeTracker[i]['sugar']
inventory.cash = lemonadeTracker[i]['cash']
inventory.start = lemonadeTracker[i]['start']
last_cmd = lemonadeTracker[i]['cmd']
lemonsLastCmd = lemonadeTracker[i]['cmd']
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
cups.cost = lemonadeCups[i]['cost']
@@ -233,21 +208,40 @@ def start_lemonade(nodeID, message, celsius=False):
potential = lemonadeWeeks[i]['potential']
unit = lemonadeWeeks[i]['unit']
price = lemonadeWeeks[i]['price']
weeks.total_sales = lemonadeWeeks[i]['total_sales']
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
score.value = lemonadeScore[i]['value']
score.total = lemonadeScore[i]['total']
logger.debug("System: Lemonade: Last Command: " + last_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 last_cmd:
# set the last command to cups in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
if newgame or "new" in lemonsLastCmd:
logger.debug("System: Lemonade: New Game: " + str(nodeID))
# Create a new display buffer for the text messages
buffer= ""
@@ -264,7 +258,7 @@ def start_lemonade(nodeID, message, celsius=False):
buffer += ". " + \
formatted + temperature.units + " " + \
forecastd[list(forecastd)[temperature.forecast]][2] + \
" " + glyph
" " + glyph + f"\n"
# Calculate the potential sales as a percentage of the maximum value
# (lower temperature = fewer sales, severe weather = fewer sales)
@@ -292,44 +286,39 @@ def start_lemonade(nodeID, message, celsius=False):
sugar.unit = round(sugar.cost / sugar.count, 2)
# Calculate the unit cost and display the estimated sales from the forecast potential
unit = cups.unit + lemons.unit + sugar.unit
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
buffer += " Sales Potential:" + str(potential) + " cups."
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
buffer += f"\nSupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
buffer += f"\nSales Potential:" + str(potential) + " cups."
# Display the current inventory
buffer += " Inventory:"
buffer += f"\nInventory:"
buffer += "🥤:" + str(inventory.cups)
buffer += "🍋:" + str(inventory.lemons)
buffer += "🍚:" + str(inventory.sugar)
# Display the updated item prices
buffer += f"\nPrices: "
buffer += "🥤:" + \
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + \
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + \
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
buffer += f"\nPrices:\n"
buffer += f"\n🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += f"\n🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += f"\n🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
# Display the current cash
gainloss = inventory.cash - inventory.start
buffer += " 💵:" + \
locale.currency(inventory.cash, grouping=True)
buffer += f"\n💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
# if the player is in the red
pnl = locale.currency(gainloss, grouping=True)
pnl = locale.currency(round(gainloss, 2), grouping=True)
if "0.00" not in pnl:
if pnl.startswith("-"):
buffer += "📊P&L📉" + pnl
else:
buffer += "📊P&L📈" + pnl
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
saveValues()
buffer += f"\n🥤 to buy?\nHave {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return buffer
if "cups" in last_cmd:
if "cups" in lemonsLastCmd and not newgame:
# Read the number of cup boxes to purchase
newcups = -1
if "n" in message.lower():
@@ -343,22 +332,22 @@ def start_lemonade(nodeID, message, celsius=False):
inventory.cups += (newcups * cups.count)
inventory.cash -= cost
msg = "Purchased " + str(newcups) + " 📦 "
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(round(inventory.cash, 2), grouping=True) + f" remaining"
else:
msg = "No 🥤 were purchased"
except Exception as e:
return "invalid input, enter the number of 🥤 to purchase or (N)one"
msg += f"\n 🍋 to buy?\nHave {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
# set the last command to lemons in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "lemons"
saveValues()
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "lemons" in last_cmd:
if "lemons" in lemonsLastCmd and not newgame:
# Read the number of lemon bags to purchase
newlemons = -1
if "n" in message.lower():
@@ -379,15 +368,15 @@ def start_lemonade(nodeID, message, celsius=False):
newlemons = -1
return "invalid input, enter the number of 🍋 to purchase"
msg += f"\n 🍚 to buy?\nYou have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
# set the last command to sugar in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sugar"
saveValues()
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "sugar" in last_cmd:
if "sugar" in lemonsLastCmd and not newgame:
# Read the number of sugar bags to purchase
newsugar = -1
if "n" in message.lower():
@@ -407,28 +396,28 @@ def start_lemonade(nodeID, message, celsius=False):
except Exception as e:
return "invalid input, enter the number of 🍚 bags to purchase"
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
msg += f"Cost of goods is {locale.currency(round(unit, 2), grouping=True)}"
msg += f"per 🥤 {locale.currency(round(inventory.cash, 2), grouping=True)} 💵 remaining."
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
# set the last command to price in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "price"
saveValues()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "price" in last_cmd:
if "price" in lemonsLastCmd and not newgame:
# set the last command to sales in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sales"
if "g" in message.lower():
lemonadeTracker[i]['cmd'] = "cups"
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
msg = f"#of🥤\nto buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
return msg
else:
last_cmd = "sales"
lemonsLastCmd = "sales"
# Read the actual price
price = 0.00
@@ -440,14 +429,15 @@ def start_lemonade(nodeID, message, celsius=False):
return "The price must be greater than zero."
except Exception as e:
price = 0.00
lemonsLastCmd = "price"
return "Invalid input, enter the price of the lemonade per 🥤"
# this isnt sent to the user, not needed
#msg = " Setting the price at " + locale.currency(price, grouping=True)
saveValues()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
if "sales" in last_cmd:
if "sales" in lemonsLastCmd and not newgame:
# Calculate the weekly sales based on price and lowest inventory level
# (higher markup price = fewer sales, limited by the inventory on-hand)
sales = get_sales_amount(potential, unit, price)
@@ -477,7 +467,7 @@ def start_lemonade(nodeID, message, celsius=False):
msg += " N.Profit:" + locale.currency(net, grouping=True)
# Display the updated inventory levels
msg += "\nRemaining"
msg += f"\nRemaining"
msg += " 🥤:" + str(inventory.cups)
msg += " 🍋:" + str(inventory.lemons)
msg += " 🍚:" + str(inventory.sugar)
@@ -493,13 +483,16 @@ def start_lemonade(nodeID, message, celsius=False):
# Display the weekly sales summary
pad_week = len(str(weeks.total))
pad_sale = len(str(weeks.sales))
total = 0
msg += "\nWeekly📊"
total = 0
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. "
total = total + weeks.summary[i]['sales']
# Update the total sales for the game
weeks.total_sales += total
# Loop through a range of prices to find the highest net profit
maxsales = 0
maxprice = 0.00
@@ -531,41 +524,43 @@ def start_lemonade(nodeID, message, celsius=False):
if (inventory.sugar <= 0):
msg += " You ran out of sugar.🍚"
else:
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
msg += f"\nCongratulations 🍋🍋 your sales were perfect!🎉"
# Increment the score counters
score.value = score.value + minnet
score.total = score.total + maxnet
# Increment the week number
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(total) + " 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('lemonade_hs.pkl', 'wb') as file:
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"
weeks.current = weeks.current + 1
msg += f"Play another week🥤? 'end' to end game"
saveValues()
lemonadeTracker[i]['last_played'] = time.time()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
else:
return "Game Over! Start a (N)ew Game or (E)xit"

View File

@@ -0,0 +1,255 @@
"""
Mesh Trekker Game
Game Rules:
1. Players compete to cover the most distance over time using their Meshtastic devices.
2. The game tracks players' movements via GPS coordinates sent by their devices.
3. Total distance traveled is calculated and summed over time for each player.
4. Leaderboards show top distances for daily, weekly, and all-time periods.
5. Players can form teams, with team distances being the sum of all team members' distances.
6. Special achievements are awarded for milestones (e.g., 10km, 50km, 100km total distance).
7. The game runs continuously, allowing players to participate at their own pace.
8. Players can use the 'whereami' command to check their current location and update their position in the game.
"""
import pickle
from modules.log import logger, getPrettyTime
from datetime import datetime, timedelta
from geopy.distance import geodesic
class MeshTrekkerError(Exception):
"""Base class for exceptions in this module."""
pass
class DataLoadError(MeshTrekkerError):
"""Raised when there's an error loading data."""
pass
class DataSaveError(MeshTrekkerError):
"""Raised when there's an error saving data."""
pass
class InvalidGPSDataError(MeshTrekkerError):
"""Raised when invalid GPS data is provided."""
pass
class MeshTrekker:
def __init__(self, data_file='mesh_trekker_data.pkl'):
self.data_file = data_file
try:
self.data = self.load_data()
except DataLoadError as e:
logger.error(f"Failed to load data: {e}")
self.data = self.initialize_data()
def initialize_data(self):
return {
'gps_data': {},
'user_distances': {},
'teams': {},
'achievements': {},
}
def load_data(self):
try:
with open(self.data_file, 'rb') as f:
return pickle.load(f)
except (pickle.PickleError, EOFError, FileNotFoundError) as e:
logger.info(f"Data file {self.data_file} not found. Initializing new data.")
return self.initialize_data()
def save_data(self):
try:
with open(self.data_file, 'wb') as f:
pickle.dump(self.data, f)
except (pickle.PickleError, IOError) as e:
raise DataSaveError(f"Error saving data: {e}")
def validate_gps_data(self, latitude, longitude, timestamp):
try:
lat = float(latitude)
lon = float(longitude)
if not -90 <= lat <= 90:
raise InvalidGPSDataError(f"Invalid latitude: {latitude}")
if not -180 <= lon <= 180:
raise InvalidGPSDataError(f"Invalid longitude: {longitude}")
if not isinstance(timestamp, datetime):
raise InvalidGPSDataError(f"Invalid timestamp: {timestamp}")
except ValueError:
raise InvalidGPSDataError(f"Invalid GPS data: latitude={latitude}, longitude={longitude}")
def process_gps_data(self, user_id, latitude, longitude, timestamp):
try:
self.validate_gps_data(latitude, longitude, timestamp)
if user_id not in self.data['gps_data']:
self.data['gps_data'][user_id] = []
self.data['gps_data'][user_id].append((float(latitude), float(longitude), timestamp))
if len(self.data['gps_data'][user_id]) > 1:
last_lat, last_lon, last_time = self.data['gps_data'][user_id][-2]
last_point = (last_lat, last_lon)
new_point = (float(latitude), float(longitude))
distance = geodesic(last_point, new_point).kilometers
if user_id not in self.data['user_distances']:
self.data['user_distances'][user_id] = (0, timestamp)
total_distance, _ = self.data['user_distances'][user_id]
new_total_distance = total_distance + distance
self.data['user_distances'][user_id] = (new_total_distance, timestamp)
self.check_achievements(user_id, new_total_distance)
self.save_data()
return new_total_distance
except InvalidGPSDataError as e:
logger.error(f"Invalid GPS data for user {user_id}: {e}")
except DataSaveError as e:
logger.error(f"Failed to save data after processing GPS for user {user_id}: {e}")
except Exception as e:
logger.error(f"Unexpected error processing GPS data for user {user_id}: {e}")
return None
def get_leaderboard(self, timeframe='all'):
try:
now = datetime.now()
if timeframe == 'daily':
start_time = now - timedelta(days=1)
elif timeframe == 'weekly':
start_time = now - timedelta(weeks=1)
else:
start_time = datetime.min
leaderboard = []
for user_id, (distance, last_updated) in self.data['user_distances'].items():
if last_updated > start_time:
leaderboard.append((user_id, distance))
return sorted(leaderboard, key=lambda x: x[1], reverse=True)[:10]
except Exception as e:
logger.error(f"Error generating leaderboard: {e}")
return []
def get_team_leaderboard(self):
try:
team_distances = {}
for team_name, members in self.data['teams'].items():
team_distance = sum(self.data['user_distances'].get(member, (0, None))[0] for member in members)
team_distances[team_name] = team_distance
return sorted(team_distances.items(), key=lambda x: x[1], reverse=True)[:10]
except Exception as e:
logger.error(f"Error generating team leaderboard: {e}")
return []
def get_user_stats(self, user_id):
try:
distance, last_updated = self.data['user_distances'].get(user_id, (0, None))
achievements = self.data['achievements'].get(user_id, [])
return {
'distance': distance,
'last_updated': last_updated,
'achievements': achievements
}
except Exception as e:
logger.error(f"Error retrieving stats for user {user_id}: {e}")
return None
def create_team(self, team_name, user_id):
try:
if team_name not in self.data['teams']:
self.data['teams'][team_name] = [user_id]
self.save_data()
return True
return False
except DataSaveError as e:
logger.error(f"Failed to save data after creating team {team_name}: {e}")
return False
except Exception as e:
logger.error(f"Error creating team {team_name}: {e}")
return False
def join_team(self, team_name, user_id):
try:
if team_name in self.data['teams'] and user_id not in self.data['teams'][team_name]:
self.data['teams'][team_name].append(user_id)
self.save_data()
return True
return False
except DataSaveError as e:
logger.error(f"Failed to save data after user {user_id} joined team {team_name}: {e}")
return False
except Exception as e:
logger.error(f"Error joining team {team_name} for user {user_id}: {e}")
return False
def check_achievements(self, user_id, total_distance):
try:
if user_id not in self.data['achievements']:
self.data['achievements'][user_id] = []
milestones = [10, 50, 100, 500, 1000] # in km
new_achievements = []
for milestone in milestones:
if total_distance >= milestone and milestone not in self.data['achievements'][user_id]:
self.data['achievements'][user_id].append(milestone)
new_achievements.append(milestone)
logger.info(f"User {user_id} achieved {milestone}km milestone!")
return new_achievements
except Exception as e:
logger.error(f"Error checking achievements for user {user_id}: {e}")
return []
def get_achievements(self, user_id):
try:
return self.data['achievements'].get(user_id, [])
except Exception as e:
logger.error(f"Error retrieving achievements for user {user_id}: {e}")
return []
# Initialize the game
game = MeshTrekker()
def handle_meshtrekker(user_id, deviceID, channel_number, location_info=(0,0)):
# Process GPS data from Meshtastic devices
latitude, longitude = location_info.split(": ")[1].split(", ")
current_time = datetime.now()
new_distance = game.process_gps_data(user_id, latitude, longitude, current_time)
# # Create and join teams
# game.create_team("Team A", "user1")
# game.join_team("Team A", "user2")
# # Get individual leaderboard
# print("\nAll-time individual leaderboard:")
# for user, distance in game.get_leaderboard():
# print(f"{user}: {distance:.2f} km")
# # Get team leaderboard
# print("\nTeam leaderboard:")
# for team, distance in game.get_team_leaderboard():
# print(f"{team}: {distance:.2f} km")
# # Get user stats
# user_stats = game.get_user_stats("user1")
# print(f"\nUser1 stats: {user_stats}")
# # Get achievements
# achievements = game.get_achievements("user1")
# print(f"User1 achievements: {achievements}")
if new_distance is not None:
new_achievements = game.check_achievements(user_id, new_distance)
response = f"{location_info}\nTotal distance: {new_distance:.2f} km"
if new_achievements:
response += f"\nNew achievements: {', '.join([f'{a}km' for a in new_achievements])}"
else:
response = f"{location_info}\nFailed to update distance. Please try again."
return response

309
modules/games/mmind.py Normal file
View File

@@ -0,0 +1,309 @@
# https://github.com/pwdkramer/pythonMastermind/blob/main/main.py
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
import pickle
from modules.log import logger
from modules.system import mindTracker
def chooseDifficultyMMind(message):
usrInput = message.lower()
msg = ''
valid_colorsMMind = "RYGB"
if not usrInput.startswith("n") and not usrInput.startswith("h") and not usrInput.startswith("x"):
# default to normal difficulty
usrInput = "n"
if usrInput == "n":
msg += f"The colors to choose from are:\nR🔴, Y🟡, G🟢, B🔵"
elif usrInput == "h":
valid_colorsMMind += "OP"
msg += f"The colors to choose from are\nR🔴, Y🟡, G🟢, B🔵, O🟠, P🟣"
elif usrInput == "x":
valid_colorsMMind += "OPWK"
msg += f"The colors to choose from are\nR🔴, Y🟡, G🟢, B🔵, O🟠, P🟣, W⚪, K⚫"
return msg
#possible colors on nomral: Red, Yellow, Green, Blue
#added colors on hard: Orange, Purple
def makeCodeMMind(diff):
secret_code = ""
for i in range(4):
if diff == "n":
roll = random.randrange(1, 5)
elif diff == "h":
roll = random.randrange(1,7)
elif diff == "x":
roll = random.randrange(1,9)
else:
print("Difficulty error in makeCode()")
if roll == 1:
secret_code += "R"
elif roll == 2:
secret_code += "Y"
elif roll == 3:
secret_code += "G"
elif roll == 4:
secret_code += "B"
elif roll == 5:
secret_code += "O"
elif roll == 6:
secret_code += "P"
elif roll == 7:
secret_code += "W"
elif roll == 8:
secret_code += "K"
else:
print("Error with range of roll in makeCode()")
return secret_code
#get guess from user
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):
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:
mindHighScore = [{'nodeID': 0, 'turns': 0, 'diff': 'n'}]
return mindHighScore
# 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 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
# 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):
# for each letter in the secret code, convert to emoji for display
secret_code = secret_code.upper()
secret_code_emoji = ""
for i in range(len(secret_code)):
if secret_code[i] == "R":
secret_code_emoji += "🔴"
elif secret_code[i] == "Y":
secret_code_emoji += "🟡"
elif secret_code[i] == "G":
secret_code_emoji += "🟢"
elif secret_code[i] == "B":
secret_code_emoji += "🔵"
elif secret_code[i] == "O":
secret_code_emoji += "🟠"
elif secret_code[i] == "P":
secret_code_emoji += "🟣"
elif secret_code[i] == "W":
secret_code_emoji += ""
elif secret_code[i] == "K":
secret_code_emoji += ""
elif secret_code[i] == "X":
secret_code_emoji += ""
return secret_code_emoji
#compare userGuess with secret code and provide feedback
def compareCodeMMind(secret_code, user_guess, nodeID):
game_won = False
perfect_pins = 0
wrong_position = 0
msg = ''
#logger.debug("System: MasterMind: secret_code: " + str(secret_code) + " user_guess: " + str(user_guess))
if secret_code == user_guess: #correct guess, user wins
perfect_pins = 4
game_won = True
else:
# check for perfect pins and right color wrong position
temp_code = []
temp_guess = []
# Check for perfect pins
for i in range(len(user_guess)):
if user_guess[i] == secret_code[i]:
perfect_pins += 1
else:
temp_code.append(secret_code[i])
temp_guess.append(user_guess[i])
# Check for right color wrong position
for guess in temp_guess:
if guess in temp_code:
wrong_position += 1
temp_code.remove(guess) # Remove the first occurrence of the matched color
# display feedback
if game_won:
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"\nGuess{getEmojiMMind(user_guess)}\n"
if perfect_pins > 0 and game_won == False:
msg += "✅ color ✅ position: {}".format(perfect_pins)
if wrong_position > 0:
if "" in msg: msg += f"\n"
msg += "✅ color 🚫 position: {}".format(wrong_position)
if "" not in msg and game_won == False:
msg += "🚫No pins in your guess😿 are in the code!"
return msg
#game loop with turn counter
def playGameMMind(diff, secret_code, turn_count, nodeID, message):
msg = ''
won = False
if turn_count <= 10:
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, nodeID)
# display turn count and feedback
msg += "Turn {}:".format(turn_count)
if check_guess.startswith("Correct"):
won = True
msg += check_guess
if won == True:
msg += f"\n🎉🧠 you win 🥷🤯"
else:
# increment turn count and keep playing
turn_count += 1
# store turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = turn_count
elif won == False:
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
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'] = 0
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
return msg
def endGameMMind(nodeID):
global mindTracker
# remove player from tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
del mindTracker[i]
logger.debug("System: MasterMind: Player removed: " + str(nodeID))
break
#main game
def start_mMind(nodeID, message):
global mindTracker
last_cmd = ""
msg = ''
# get player's last command from tracker if not new player
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
last_cmd = mindTracker[i]['cmd']
if last_cmd == "new":
if message.lower().startswith("n") or message.lower().startswith("h") or message.lower().startswith("x"):
diff = message.lower()[0]
else:
diff = "n"
# set player's last command to makeCode
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['cmd'] = 'makeCode'
mindTracker[i]['diff'] = diff
# Return color message to player
msg += chooseDifficultyMMind(message.lower()[0])
return msg
if last_cmd == "makeCode":
# get difficulty from tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
diff = mindTracker[i]['diff']
secret_code = makeCodeMMind(diff)
last_cmd = "playGame"
# set player's last command to playGame
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['cmd'] = 'playGame'
mindTracker[i]['secret_code'] = secret_code
mindTracker[i]['last_played'] = time.time()
if last_cmd == "playGame":
# get difficulty, secret code, and turn count from tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
diff = mindTracker[i]['diff']
secret_code = mindTracker[i]['secret_code']
turn_count = mindTracker[i]['turns']
msg += playGameMMind(diff, secret_code, turn_count, nodeID=nodeID, message=message)
return msg

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

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

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

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

View File

@@ -2,13 +2,13 @@
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
from modules.log import *
import pickle
from modules.log import logger, getPrettyTime
vpStartingCash = 20
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
# Define the Card class
class CardVP:
global vpTracker
card_values = { # value of the ace is high until it needs to be low
2: 2,
@@ -54,10 +54,8 @@ def drawCardsVp(*cards, return_string=True):
# "King" should be "K" and "10" should still be "10"
if card.rank == 10: # ten is the only one who's rank is 2 char long
rank = str(card.rank)
space = '' # if we write "10" on the card that line will be 1 char to long
else:
rank = str(card.rank)[0] # some have a rank of 'King' this changes that to a simple 'K' ("King" doesn't fit)
space = ' ' # no "10", we use a blank space to will the void
# get the cards suit in two steps
suit = suits_name.index(card.suit)
suit = suits_symbols[suit]
@@ -165,8 +163,8 @@ class PlayerVP:
return self.show_hand()
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
def score_hand(self, resetHand = True):
@@ -232,7 +230,6 @@ class PlayerVP:
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif 2 in points_repeat: # find pair
logger.debug(f"System: VideoPoker: 235 self.bankroll: {self.bankroll} bet_size: {self.bet_size}")
hand_name = "Pair👯"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
@@ -274,153 +271,184 @@ def setLastCmdVp(nodeID, cmd):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['cmd'] = cmd
def saveHSVp(nodeID, highScore):
# Save the game high_score to pickle
highScore = {'nodeID': nodeID, 'highScore': highScore}
try:
with open('data/videopoker_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new data/videopoker_hs.pkl file")
with open('data/videopoker_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
def loadHSVp():
# Load the game high_score from pickle
try:
with open('data/videopoker_hs.pkl', 'rb') as file:
highScore = pickle.load(file)
return highScore
except FileNotFoundError:
logger.debug("System: VideoPoker: Creating new data/videopoker_hs.pkl file")
highScore = {'nodeID': 0, 'highScore': 0}
with open('data/videopoker_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
return 0
def playVideoPoker(nodeID, message):
global vpTracker, vpStartingCash
msg = ""
try:
# Initialize the player
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID) == "":
# 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"You have {vpStartingCash} coins, \nWhats your bet?"
# Gather the player's bet
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
# Initialize shuffled Deck and Player
player = PlayerVP()
deck = DeckVP()
deck.shuffle()
drawCount = 1
bet = 0
msg = ''
# Initialize the player
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID=nodeID) == "":
# 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?"
# Gather the player's bet
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
# Initialize shuffled Deck and Player
player = PlayerVP()
deck = DeckVP()
deck.shuffle()
drawCount = 1
bet = 0
msg = ''
# load the player bankroll from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player.bankroll = vpTracker[i]['cash']
vpTracker[i]['time'] = time.time()
# Detect if message is a bet
try:
bet = int(message)
except ValueError:
msg += "Please enter a valid bet amount. 1 to 5 coins."
# Check if bet is valid
if bet > player.bankroll:
msg += "You can only bet the money you have. No strip poker here..."
elif bet < 1:
msg += "You must bet at least 1 coin."
elif bet > 5:
msg += "You can only bet up to 5 coins."
# if msg contains an error, return it
if msg is not None and msg != '':
return msg
else:
# Take the bet
player.bet(str(message))
# Bet placed, start the game
setLastCmdVp(nodeID, "playing")
# save player and deck to tracker
# load the player bankroll from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['cash'] = player.bankroll
player.bankroll = vpTracker[i]['cash']
vpTracker[i]['time'] = time.time()
# Play the game
if getLastCmdVp(nodeID) == "playing":
msg = ''
player.draw_cards(deck)
msg += player.show_hand()
# give hint to player
msg += player.score_hand(resetHand=False)
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
# Detect if message is a bet
try:
bet = int(message)
except ValueError:
msg += f"Please enter a valid bet, 1 to 5 coins. you have {player.bankroll} coins."
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll"
setLastCmdVp(nodeID, "redraw")
return msg
if getLastCmdVp(nodeID) == "redraw":
msg = ''
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
drawCount = vpTracker[i]['drawCount']
# Check if bet is valid
if bet > player.bankroll:
msg += f"You can only bet the money you have. {player.bankroll} coins, No strip poker here..."
elif bet < 1:
msg += "You must bet at least 1 coin.🪙"
elif bet > 5:
msg += "The 🎰 coin slot only fits 5 coins max."
# if msg contains an error, return it
if msg is not None and msg != '':
return msg
else:
# Take the bet
player.bet(str(message))
# Bet placed, start the game
setLastCmdVp(nodeID, "playing")
# if player wants to redraw cards, and not done already
if message.lower().startswith("n"):
setLastCmdVp(nodeID, "endGame")
if message.lower().startswith("h"):
msg = player.show_hand()
return msg
else:
if drawCount <= 1:
msg = player.redraw(deck, message)
if msg.startswith("Send Card"):
# if returned error message, return it
return msg
drawCount += 1
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
if drawCount == 2:
# this is the last draw will carry on to endGame for scoring
msg = player.redraw(deck, message) + f"\n"
if msg.startswith("Send Card"):
vpTracker[i]['cash'] = player.bankroll
# Play the game
if getLastCmdVp(nodeID) == "playing":
msg = ''
player.draw_cards(deck)
msg += player.show_hand()
# give hint to player
msg += player.score_hand(resetHand=False)
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll (H)and"
setLastCmdVp(nodeID, "redraw")
return msg
if getLastCmdVp(nodeID) == "redraw":
msg = ''
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
drawCount = vpTracker[i]['drawCount']
# if player wants to redraw cards, and not done already
if message.lower().startswith("n"):
setLastCmdVp(nodeID, "endGame")
if message.lower().startswith("h"):
msg = player.show_hand()
return msg
else:
if drawCount <= 1:
msg = player.redraw(deck, message)
if msg.startswith("ex:"):
# if returned error message, return it
return msg
# redraw done
setLastCmdVp(nodeID, "endGame")
drawCount += 1
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
if drawCount == 2:
# this is the last draw will carry on to endGame for scoring
msg = player.redraw(deck, message) + f"\n"
if msg.startswith("ex:"):
# if returned error message, return it
return msg
# redraw done
setLastCmdVp(nodeID, "endGame")
else:
# show redrawn hand
return msg
else:
# show redrawn hand
return msg
else:
# redraw already done
setLastCmdVp(nodeID, "endGame")
if getLastCmdVp(nodeID) == "endGame":
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
# redraw already done
setLastCmdVp(nodeID, "endGame")
if getLastCmdVp(nodeID) == "endGame":
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
msg += player.score_hand()
msg += player.score_hand()
if player.bankroll < 1:
player.bankroll = vpStartingCash
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
elif player.bankroll > vpTracker[i]['highScore']:
vpTracker[i]['highScore'] = player.bankroll
msg += " 🎉HighScore!"
if player.bankroll < 1:
player.bankroll = vpStartingCash
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
elif player.bankroll > vpTracker[i]['highScore']:
vpTracker[i]['highScore'] = player.bankroll
msg += " 🎉HighScore!"
# save high score
saveHSVp(nodeID, vpTracker[i]['highScore'])
msg += f"\nPlace your Bet, 'L' to leave the game."
msg += f"\nPlace your Bet, or (L)eave Table."
setLastCmdVp(nodeID, "gameOver")
# reset player and deck in tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = None
vpTracker[i]['deck'] = None
vpTracker[i]['drawCount'] = 0
# save bankroll
vpTracker[i]['cash'] = player.bankroll
setLastCmdVp(nodeID, "gameOver")
# reset player and deck in tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = None
vpTracker[i]['deck'] = None
vpTracker[i]['drawCount'] = 0
# save bankroll
vpTracker[i]['cash'] = player.bankroll
return msg
return msg
# At the end of the try block, if nothing returned yet:
return msg if msg else 'No action taken.'
except Exception as e:
logger.warning(f"System: VideoPoker: Error {e}")
return 'No Game in progress'

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 logger, getPrettyTime
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']}")

133
modules/globalalert.py Normal file
View File

@@ -0,0 +1,133 @@
# helper functions to use location data for data outside US/north america
# K7MHI Kelly Keeton 2024
import json # pip install json
#from geopy.geocoders import Nominatim # pip install geopy
#import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
#import xml.dom.minidom
from modules.log import logger
from modules.settings import urlTimeoutSeconds, NO_ALERTS, myRegionalKeysDE
trap_list_location_eu = ("ukalert",)
trap_list_location_de = ("dealert",)
def get_govUK_alerts(lat, lon):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'
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')
except Exception as e:
logger.warning("Error getting UK alerts: " + str(e))
return
if alert:
return "🚨" + alert.get_text(strip=True)
else:
return NO_ALERTS
def get_nina_alerts():
try:
# get api.bund.dev alerts
alerts = []
for regionalKey in myRegionalKeysDE:
url = ("https://nina.api.proxy.bund.dev/api31/dashboard/" + regionalKey + ".json")
response = requests.get(url, timeout=urlTimeoutSeconds)
data = response.json()
for item in data:
title = item["i18nTitle"]["de"]
alerts.append(f"🚨 {title}")
return "\n".join(alerts) if alerts else NO_ALERTS
except Exception as e:
logger.warning("Error getting NINA DE alerts: " + str(e))
return NO_ALERTS
def get_wxUKgov():
# get UK weather warnings, these look icky
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
try:
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
response = requests.get(url, timeout=urlTimeoutSeconds)
soup = bs.BeautifulSoup(response.content, 'xml')
items = soup.find_all('item')
alerts = []
for item in items:
title = item.find('title').get_text(strip=True)
description = item.find('description').get_text(strip=True)
alerts.append(f"🚨 {title}: {description}")
return "\n".join(alerts) if alerts else NO_ALERTS
except Exception as e:
logger.warning("Error getting UK weather warnings: " + str(e))
return NO_ALERTS
def get_floodUKgov():
# get UK flood warnings, there is so much I need a locals help
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
return NO_ALERTS
def get_crimeUKgov(lat, lon):
"""
Fetches recent street crime data from UK Police API for given lat/lon.
Returns a summary string or NO_ALERTS. -- pay for use?
"""
date = datetime.datetime.now().strftime("%Y-%m")
url = f'https://data.police.uk/api/crimes-street/all-crime?date={date}&lat={lat}&lng={lon}'
try:
response = requests.get(url, timeout=urlTimeoutSeconds)
if not response.ok or not response.text.strip():
return NO_ALERTS
crimes = response.json()
if not crimes:
return NO_ALERTS
# Summarize the first few crimes
summaries = []
for crime in crimes[:3]:
category = crime.get("category", "Unknown")
outcome = crime.get("outcome_status", {}).get("category", "No outcome")
location = crime.get("location", {}).get("street", {}).get("name", "Unknown location")
summaries.append(f"{category.title()} at {location} ({outcome})")
return "\n".join(summaries)
except Exception as e:
logger.warning(f"Error fetching UK crime data: {e}")
return NO_ALERTS
def get_crime_stopsUKgov(lat, lon):
"""
Fetches recent stop-and-search data from UK Police API for given lat/lon.
Returns a summary string or NO_ALERTS. -- pay for use?
"""
date = datetime.datetime.now().strftime("%Y-%m")
url = f'https://data.police.uk/api/stops-street?date={date}&lat={lat}&lng={lon}'
try:
response = requests.get(url, timeout=urlTimeoutSeconds)
if not response.ok or not response.text.strip():
return NO_ALERTS
stops = response.json()
if not stops:
return NO_ALERTS
# Summarize the first few stops
summaries = []
for stop in stops[:3]: # Limit to first 3 stops for brevity
summary = (
f"Date: {stop.get('datetime', 'N/A')}, "
f"Outcome: {stop.get('outcome', 'N/A')}, "
f"Ethnicity: {stop.get('self_defined_ethnicity', 'N/A')}, "
f"Gender: {stop.get('gender', 'N/A')}, "
f"Location: {stop.get('location', {}).get('street', {}).get('name', 'N/A')}"
)
summaries.append(summary)
return "\n".join(summaries)
except Exception as e:
return NO_ALERTS

73
modules/gpio.py Normal file
View File

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

423
modules/inventory.md Normal file
View File

@@ -0,0 +1,423 @@
# Inventory & Point of Sale System
## Overview
The inventory module provides a simple point-of-sale (POS) system for mesh networks, enabling inventory management, sales tracking, and cart-based transactions. This system is ideal for:
- Emergency supply management
- Event merchandise sales
- Community supply tracking
- Remote location inventory
- Asset management
- Field operations logistics
- Tool lending in makerspaces or ham swaps
- Tracking and lending shared items like Legos or kits
> **Tool Lending & Shared Item Tracking:**
> The system supports lending out tools or kits (e.g., in a makerspace or ham swap) using the `itemloan` and `itemreturn` commands. You can also track bulk or set-based items like Legos, manage their locations, and log checkouts and returns for community sharing or events.
## Features
### 🏪 Simple POS System
- **Item Management**: Add, remove, and update inventory items
- **Cart System**: Build orders before completing transactions
- **Transaction Logging**: Full audit trail of all sales and returns
- **Price Tracking**: Track price changes over time
- **Location Tracking**: Optional warehouse/location field for items
### 💰 Financial Features
- **Penny Rounding**: USA cash sales support
- Cash sales round down to nearest nickel
- Taxed sales round up to nearest nickel
- **Daily Statistics**: Track sales performance
- **Hot Item Detection**: Identify best-selling products
- **Revenue Tracking**: Daily sales totals
### 📊 Reporting
- **Inventory Value**: Total inventory worth
- **Sales Reports**: Daily transaction summaries
- **Best Sellers**: Most popular items
**Cart System:**
- `cartadd <name> <qty>` - Add to cart
- `cartremove <name>` - Remove from cart
- `cartlist` / `cart` - View cart
- `cartbuy` / `cartsell [notes]` - Complete transaction
- `cartclear` - Empty cart
**Item Management:**
- `itemadd <name> <qty> [price] [loc]` - Add new item
- `itemremove <name>` - Remove item
- `itemreset name> <qty> [price] [loc]` - Update item
- `itemsell <name> <qty> [notes]` - Quick sale
- `itemloan <name> <note>` - Loan/checkout an item
- `itemreturn <transaction_id>` - Reverse transaction
- `itemlist` - View all inventory
- `itemstats` - Daily statistics
## Configuration
Add to your `config.ini`:
```ini
[inventory]
enabled = True
inventory_db = data/inventory.db
# Set to True to disable penny precision and round to nickels (USA cash sales)
# When True: cash sales round down, taxed sales round up to nearest $0.05
# When False (default): normal penny precision ($0.01)
disable_penny = False
```
## Commands Reference
### Item Management
#### Add Item
```
itemadd <name> <price> <quantity> [location]
```
Adds a new item to inventory.
**Examples:**
```
itemadd Radio 149.99 5 Shelf-A
itemadd Battery 12.50 20 Warehouse
itemadd Water 1.00 100
```
#### Remove Item
```
itemremove <name>
```
Removes an item from inventory (also removes from all carts).
**Examples:**
```
itemremove Radio
itemremove "First Aid Kit"
```
#### Update Item
```
itemreset <name> [price=X] [qty=Y]
```
Updates item price and/or quantity.
**Examples:**
```
itemreset Radio price=139.99
itemreset Battery qty=50
itemreset Water price=0.95 qty=200
```
#### Quick Sale
```
itemsell <name> <quantity> [notes]
```
Sell directly without using cart (for quick transactions).
**Examples:**
```
itemsell Battery 2
itemsell Water 10 Emergency supply
itemsell Radio 1 Field unit sale
```
#### Return Transaction
```
itemreturn <transaction_id>
```
Reverse a transaction and return items to inventory.
**Examples:**
```
itemreturn 123
itemreturn 45
```
#### List Inventory
```
itemlist
```
Shows all items with prices, quantities, and total inventory value.
**Example Response:**
```
📦 Inventory:
Radio: $149.99 x 5 @ Shelf-A = $749.95
Battery: $12.50 x 20 @ Warehouse = $250.00
Water: $1.00 x 100 = $100.00
Total Value: $1,099.95
```
#### Statistics
```
itemstats
```
Shows today's sales performance.
**Example Response:**
```
📊 Today's Stats:
Sales: 15
Revenue: $423.50
Hot Item: Battery (8 sold)
```
### Cart System
#### Add to Cart
```
cartadd <name> <quantity>
```
Add items to your shopping cart.
**Examples:**
```
cartadd Radio 2
cartadd Battery 4
cartadd Water 12
```
#### Remove from Cart
```
cartremove <name>
```
Remove items from cart.
**Examples:**
```
cartremove Radio
cartremove Battery
```
#### View Cart
```
cart
cartlist
```
Display your current cart contents and total.
**Example Response:**
```
🛒 Your Cart:
Radio: $149.99 x 2 = $299.98
Battery: $12.50 x 4 = $50.00
Total: $349.98
```
#### Complete Transaction
```
cartbuy [notes]
cartsell [notes]
```
Process the cart as a transaction. Use `cartbuy` for purchases (adds to inventory) or `cartsell` for sales (removes from inventory).
**Examples:**
```
cartsell Customer purchase
cartbuy Restocking supplies
cartsell Event merchandise
```
#### Clear Cart
```
cartclear
```
Empty your shopping cart without completing a transaction.
## Use Cases
### 1. Event Merchandise Sales
Perfect for festivals, hamfests, or community events:
```
# Setup inventory
itemadd Tshirt 20.00 50 Booth-A
itemadd Hat 15.00 30 Booth-A
itemadd Sticker 5.00 100 Booth-B
# Customer transaction
cartadd Tshirt 2
cartadd Hat 1
cartsell Festival sale
# Check daily performance
itemstats
```
### 2. Emergency Supply Tracking
Track supplies during disaster response:
```
# Add emergency supplies
itemadd Water 0.00 500 Warehouse-1
itemadd MRE 0.00 200 Warehouse-1
itemadd Blanket 0.00 100 Warehouse-2
# Distribute supplies
itemsell Water 50 Red Cross distribution
itemsell MRE 20 Family shelter
# Check remaining inventory
itemlist
```
### 3. Field Equipment Management
Manage tools and equipment in remote locations:
```
# Track equipment
itemadd Generator 500.00 3 Base-Camp
itemadd Radio 200.00 10 Equipment-Room
itemadd Battery 15.00 50 Supply-Closet
# Equipment checkout
itemsell Generator 1 Field deployment
itemsell Radio 5 Survey team
# Monitor inventory
itemlist
itemstats
```
### 4. Community Supply Exchange
Facilitate supply exchanges within a community:
```
# Add community items
itemadd Seeds 2.00 100 Community-Garden
itemadd Firewood 10.00 20 Storage-Shed
# Member transactions
cartadd Seeds 5
cartadd Firewood 2
cartsell Member-123 purchase
```
## Penny Rounding (USA Mode)
When `disable_penny = True` is set in the configuration, the system implements penny rounding (disabling penny precision). This follows USA practice where pennies are not commonly used in cash transactions.
### Cash Sales (Round Down)
- $10.47 → $10.45
- $10.48 → $10.45
- $10.49 → $10.45
### Taxed Sales (Round Up)
- $10.47 → $10.50
- $10.48 → $10.50
- $10.49 → $10.50
This follows common USA practice where pennies are not used in cash transactions.
## Database Schema
The system uses SQLite with four tables:
### items
```sql
CREATE TABLE items (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_name TEXT UNIQUE NOT NULL,
item_price REAL NOT NULL,
item_quantity INTEGER NOT NULL DEFAULT 0,
location TEXT,
created_date TEXT,
updated_date TEXT
)
```
### transactions
```sql
CREATE TABLE transactions (
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_type TEXT NOT NULL,
transaction_date TEXT NOT NULL,
transaction_time TEXT NOT NULL,
user_name TEXT,
total_amount REAL NOT NULL,
notes TEXT
)
```
### transaction_items
```sql
CREATE TABLE transaction_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price_at_sale REAL NOT NULL,
FOREIGN KEY (transaction_id) REFERENCES transactions(transaction_id),
FOREIGN KEY (item_id) REFERENCES items(item_id)
)
```
### carts
```sql
CREATE TABLE carts (
cart_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
added_date TEXT,
FOREIGN KEY (item_id) REFERENCES items(item_id)
)
```
## Security Considerations
- Users on the `bbs_ban_list` cannot use inventory commands
- Each user has their own cart (identified by node ID)
- Transactions are logged with user information for accountability
- All database operations use parameterized queries to prevent SQL injection
## Tips and Best Practices
1. **Regular Inventory Checks**: Use `itemlist` regularly to monitor stock levels
2. **Descriptive Notes**: Add notes to transactions for better tracking
3. **Location Tags**: Use consistent location naming for better organization
4. **Daily Reviews**: Check `itemstats` at the end of each day
5. **Transaction IDs**: Keep track of transaction IDs for potential returns
6. **Quantity Updates**: Use `itemreset` to adjust inventory after physical counts
7. **Cart Cleanup**: Use `cartclear` if you change your mind before completing a sale
## Troubleshooting
### Item Already Exists
If you get "Item already exists" when using `itemadd`, use `itemreset` instead to update the existing item.
### Insufficient Quantity
If you see "Insufficient quantity" error, check available stock with `itemlist` before attempting the sale.
### Transaction Not Found
If `itemreturn` fails, verify the transaction ID exists. Use recent transaction logs to find valid IDs.
### Cart Not Showing Items
Each user has their own cart. Make sure you're using your own node to view your cart.
## Support
For issues or feature requests, please file an issue on the GitHub repository.

747
modules/inventory.py Normal file
View File

@@ -0,0 +1,747 @@
# Inventory and Point of Sale module for the bot
# K7MHI Kelly Keeton 2024
# Enhanced POS system with cart management and inventory tracking
import sqlite3
from modules.log import logger
from modules.settings import inventory_db, disable_penny, bbs_ban_list
import time
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN
trap_list_inventory = ("item", "itemlist", "itemloan", "itemsell", "itemreturn", "itemadd", "itemremove",
"itemreset", "itemstats", "cart", "cartadd", "cartremove", "cartlist",
"cartbuy", "cartsell", "cartclear")
def initialize_inventory_database():
"""Initialize the inventory database with all necessary tables"""
try:
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
# Items table - stores inventory items
logger.debug("System: Inventory: Initializing database...")
c.execute('''CREATE TABLE IF NOT EXISTS items
(item_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_name TEXT UNIQUE NOT NULL,
item_price REAL NOT NULL,
item_quantity INTEGER NOT NULL DEFAULT 0,
location TEXT,
created_date TEXT,
updated_date TEXT)''')
# Transactions table - stores sales/purchases
c.execute('''CREATE TABLE IF NOT EXISTS transactions
(transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_type TEXT NOT NULL,
transaction_date TEXT NOT NULL,
transaction_time TEXT NOT NULL,
user_name TEXT,
total_amount REAL NOT NULL,
notes TEXT)''')
# Transaction items table - stores items in each transaction
c.execute('''CREATE TABLE IF NOT EXISTS transaction_items
(id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price_at_sale REAL NOT NULL,
FOREIGN KEY (transaction_id) REFERENCES transactions(transaction_id),
FOREIGN KEY (item_id) REFERENCES items(item_id))''')
# Carts table - stores temporary shopping carts
c.execute('''CREATE TABLE IF NOT EXISTS carts
(cart_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
added_date TEXT,
FOREIGN KEY (item_id) REFERENCES items(item_id))''')
conn.commit()
conn.close()
logger.info("Inventory: Database initialized successfully")
return True
except Exception as e:
logger.error(f"Inventory: Failed to initialize database: {e}")
return False
def round_price(amount, is_taxed_sale=False):
"""Round price based on penny rounding settings"""
if not disable_penny:
return float(Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# Penny rounding logic
decimal_amount = Decimal(str(amount))
if is_taxed_sale:
# Round up for taxed sales
return float(decimal_amount.quantize(Decimal('0.05'), rounding=ROUND_HALF_UP))
else:
# Round down for cash sales
return float(decimal_amount.quantize(Decimal('0.05'), rounding=ROUND_DOWN))
def add_item(name, price, quantity=0, location=""):
"""Add a new item to inventory"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Check if item already exists
c.execute("SELECT item_id FROM items WHERE item_name = ?", (name,))
existing = c.fetchone()
if existing:
conn.close()
return f"Item '{name}' already exists. Use itemreset to update."
c.execute("""INSERT INTO items (item_name, item_price, item_quantity, location, created_date, updated_date)
VALUES (?, ?, ?, ?, ?, ?)""",
(name, price, quantity, location, current_date, current_date))
conn.commit()
conn.close()
return f"✅ Item added: {name} - ${price:.2f} - Qty: {quantity}"
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_inventory_database()
return add_item(name, price, quantity, location)
else:
conn.close()
logger.error(f"Inventory: Error adding item: {e}")
return "Error adding item."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error adding item: {e}")
return "Error adding item."
def remove_item(name):
"""Remove an item from inventory"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("DELETE FROM items WHERE item_name = ?", (name,))
if c.rowcount == 0:
conn.close()
return f"Item '{name}' not found."
conn.commit()
conn.close()
return f"🗑️ Item removed: {name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error removing item: {e}")
return "Error removing item."
def reset_item(name, price=None, quantity=None):
"""Update item price or quantity"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Check if item exists
c.execute("SELECT item_price, item_quantity FROM items WHERE item_name = ?", (name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{name}' not found."
updates = []
params = []
if price is not None:
updates.append("item_price = ?")
params.append(price)
if quantity is not None:
updates.append("item_quantity = ?")
params.append(quantity)
if not updates:
conn.close()
return "No updates specified."
updates.append("updated_date = ?")
params.append(current_date)
params.append(name)
query = f"UPDATE items SET {', '.join(updates)} WHERE item_name = ?"
c.execute(query, params)
conn.commit()
conn.close()
update_msg = []
if price is not None:
update_msg.append(f"Price: ${price:.2f}")
if quantity is not None:
update_msg.append(f"Qty: {quantity}")
return f"🔄 Item updated: {name} - {' - '.join(update_msg)}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error resetting item: {e}")
return "Error updating item."
def sell_item(name, quantity, user_name="", notes=""):
"""Sell an item (remove from inventory and record transaction)"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
try:
# Get item details
c.execute("SELECT item_id, item_price, item_quantity FROM items WHERE item_name = ?", (name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{name}' not found."
item_id, price, current_qty = item
if current_qty < quantity:
conn.close()
return f"Insufficient quantity. Available: {current_qty}"
# Calculate total with rounding
total = round_price(price * quantity, is_taxed_sale=True)
# Create transaction
c.execute("""INSERT INTO transactions (transaction_type, transaction_date, transaction_time,
user_name, total_amount, notes)
VALUES (?, ?, ?, ?, ?, ?)""",
("SALE", current_date, current_time, user_name, total, notes))
transaction_id = c.lastrowid
# Add transaction item
c.execute("""INSERT INTO transaction_items (transaction_id, item_id, quantity, price_at_sale)
VALUES (?, ?, ?, ?)""",
(transaction_id, item_id, quantity, price))
# Update inventory
c.execute("UPDATE items SET item_quantity = item_quantity - ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
conn.commit()
conn.close()
return f"💰 Sale: {quantity}x {name} - Total: ${total:.2f}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error selling item: {e}")
return "Error processing sale."
def return_item(transaction_id):
"""Return items from a transaction (reverse the sale or loan)"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Get transaction details
c.execute("SELECT transaction_type FROM transactions WHERE transaction_id = ?", (transaction_id,))
transaction = c.fetchone()
if not transaction:
conn.close()
return f"Transaction {transaction_id} not found."
transaction_type = transaction[0]
# Get items in transaction
c.execute("""SELECT ti.item_id, ti.quantity, i.item_name
FROM transaction_items ti
JOIN items i ON ti.item_id = i.item_id
WHERE ti.transaction_id = ?""", (transaction_id,))
items = c.fetchall()
if not items:
conn.close()
return f"No items found for transaction {transaction_id}."
# Return items to inventory
for item_id, quantity, item_name in items:
c.execute("UPDATE items SET item_quantity = item_quantity + ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
# Remove transaction and transaction_items
c.execute("DELETE FROM transactions WHERE transaction_id = ?", (transaction_id,))
c.execute("DELETE FROM transaction_items WHERE transaction_id = ?", (transaction_id,))
conn.commit()
conn.close()
if transaction_type == "LOAN":
return f"↩️ Loan {transaction_id} returned. Item(s) back in inventory."
else:
return f"↩️ Transaction {transaction_id} reversed. Items returned to inventory."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error returning item: {e}")
return "Error processing return."
def loan_item(name, user_name="", note=""):
"""Loan an item (checkout/loan to someone, record transaction)"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
try:
# Get item details
c.execute("SELECT item_id, item_price, item_quantity FROM items WHERE item_name = ?", (name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{name}' not found."
item_id, price, current_qty = item
if current_qty < 1:
conn.close()
return f"Insufficient quantity. Available: {current_qty}"
# Create loan transaction (quantity always 1 for now)
c.execute("""INSERT INTO transactions (transaction_type, transaction_date, transaction_time,
user_name, total_amount, notes)
VALUES (?, ?, ?, ?, ?, ?)""",
("LOAN", current_date, current_time, user_name, 0.0, note))
transaction_id = c.lastrowid
# Add transaction item
c.execute("""INSERT INTO transaction_items (transaction_id, item_id, quantity, price_at_sale)
VALUES (?, ?, ?, ?)""",
(transaction_id, item_id, 1, price))
# Update inventory
c.execute("UPDATE items SET item_quantity = item_quantity - 1, updated_date = ? WHERE item_id = ?",
(current_date, item_id))
conn.commit()
conn.close()
return f"🔖 Loaned: {name} (note: {note}) [Transaction #{transaction_id}]"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error loaning item: {e}")
return "Error processing loan."
def get_loans_for_items():
"""Return a dict of item_name -> list of loan notes for currently loaned items"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
# Find all active loans (not returned)
c.execute("""
SELECT i.item_name, t.notes
FROM transactions t
JOIN transaction_items ti ON t.transaction_id = ti.transaction_id
JOIN items i ON ti.item_id = i.item_id
WHERE t.transaction_type = 'LOAN'
""")
rows = c.fetchall()
conn.close()
loans = {}
for item_name, note in rows:
loans.setdefault(item_name, []).append(note)
return loans
except Exception as e:
conn.close()
logger.error(f"Inventory: Error fetching loans: {e}")
return {}
def list_items():
"""List all items in inventory, with loan info if any"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("SELECT item_name, item_price, item_quantity, location FROM items ORDER BY item_name")
items = c.fetchall()
conn.close()
if not items:
return "No items in inventory."
# Get loan info
loans = get_loans_for_items()
result = "📦 Inventory:\n"
total_value = 0
for name, price, qty, location in items:
value = price * qty
total_value += value
loc_str = f" @ {location}" if location else ""
loan_str = ""
if name in loans:
for note in loans[name]:
loan_str += f" [loan: {note}]"
result += f"{name}: ${price:.2f} x {qty}{loc_str} = ${value:.2f}{loan_str}\n"
result += f"\nTotal Value: ${total_value:.2f}"
return result.rstrip()
except Exception as e:
conn.close()
logger.error(f"Inventory: Error listing items: {e}")
return "Error listing items."
def get_stats():
"""Get sales statistics"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
current_date = time.strftime("%Y-%m-%d")
# Get today's sales
c.execute("""SELECT COUNT(*), SUM(total_amount)
FROM transactions
WHERE transaction_type = 'SALE' AND transaction_date = ?""",
(current_date,))
today_stats = c.fetchone()
today_count = today_stats[0] or 0
today_total = today_stats[1] or 0
# Get hot item (most sold today)
c.execute("""SELECT i.item_name, SUM(ti.quantity) as total_qty
FROM transaction_items ti
JOIN transactions t ON ti.transaction_id = t.transaction_id
JOIN items i ON ti.item_id = i.item_id
WHERE t.transaction_date = ? AND t.transaction_type = 'SALE'
GROUP BY i.item_name
ORDER BY total_qty DESC
LIMIT 1""", (current_date,))
hot_item = c.fetchone()
conn.close()
result = f"📊 Today's Stats:\n"
result += f"Sales: {today_count}\n"
result += f"Revenue: ${today_total:.2f}\n"
if hot_item:
result += f"Hot Item: {hot_item[0]} ({hot_item[1]} sold)"
else:
result += "Hot Item: None"
return result
except Exception as e:
conn.close()
logger.error(f"Inventory: Error getting stats: {e}")
return "Error getting stats."
def add_to_cart(user_id, item_name, quantity):
"""Add item to user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Get item details
c.execute("SELECT item_id, item_quantity FROM items WHERE item_name = ?", (item_name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{item_name}' not found."
item_id, available_qty = item
# Check if item already in cart
c.execute("SELECT quantity FROM carts WHERE user_id = ? AND item_id = ?", (user_id, item_id))
existing = c.fetchone()
if existing:
new_qty = existing[0] + quantity
if new_qty > available_qty:
conn.close()
return f"Insufficient quantity. Available: {available_qty}"
c.execute("UPDATE carts SET quantity = ? WHERE user_id = ? AND item_id = ?",
(new_qty, user_id, item_id))
else:
if quantity > available_qty:
conn.close()
return f"Insufficient quantity. Available: {available_qty}"
c.execute("INSERT INTO carts (user_id, item_id, quantity, added_date) VALUES (?, ?, ?, ?)",
(user_id, item_id, quantity, current_date))
conn.commit()
conn.close()
return f"🛒 Added to cart: {quantity}x {item_name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error adding to cart: {e}")
return "Error adding to cart."
def remove_from_cart(user_id, item_name):
"""Remove item from user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("""DELETE FROM carts
WHERE user_id = ? AND item_id = (SELECT item_id FROM items WHERE item_name = ?)""",
(user_id, item_name))
if c.rowcount == 0:
conn.close()
return f"Item '{item_name}' not in cart."
conn.commit()
conn.close()
return f"🗑️ Removed from cart: {item_name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error removing from cart: {e}")
return "Error removing from cart."
def list_cart(user_id):
"""List items in user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("""SELECT i.item_name, i.item_price, c.quantity
FROM carts c
JOIN items i ON c.item_id = i.item_id
WHERE c.user_id = ?""", (user_id,))
items = c.fetchall()
conn.close()
if not items:
return "🛒 Cart is empty."
result = "🛒 Your Cart:\n"
total = 0
for name, price, qty in items:
subtotal = price * qty
total += subtotal
result += f"{name}: ${price:.2f} x {qty} = ${subtotal:.2f}\n"
total = round_price(total, is_taxed_sale=True)
result += f"\nTotal: ${total:.2f}"
return result
except Exception as e:
conn.close()
logger.error(f"Inventory: Error listing cart: {e}")
return "Error listing cart."
def checkout_cart(user_id, user_name="", transaction_type="SALE", notes=""):
"""Process cart as a transaction"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
try:
# Get cart items
c.execute("""SELECT i.item_id, i.item_name, i.item_price, c.quantity, i.item_quantity
FROM carts c
JOIN items i ON c.item_id = i.item_id
WHERE c.user_id = ?""", (user_id,))
cart_items = c.fetchall()
if not cart_items:
conn.close()
return "Cart is empty."
# Verify all items have sufficient quantity
for item_id, name, price, cart_qty, stock_qty in cart_items:
if stock_qty < cart_qty:
conn.close()
return f"Insufficient quantity for '{name}'. Available: {stock_qty}"
# Calculate total
total = sum(price * qty for _, _, price, qty, _ in cart_items)
total = round_price(total, is_taxed_sale=(transaction_type == "SALE"))
# Create transaction
c.execute("""INSERT INTO transactions (transaction_type, transaction_date, transaction_time,
user_name, total_amount, notes)
VALUES (?, ?, ?, ?, ?, ?)""",
(transaction_type, current_date, current_time, user_name, total, notes))
transaction_id = c.lastrowid
# Process each item
for item_id, name, price, quantity, _ in cart_items:
# Add to transaction items
c.execute("""INSERT INTO transaction_items (transaction_id, item_id, quantity, price_at_sale)
VALUES (?, ?, ?, ?)""",
(transaction_id, item_id, quantity, price))
# Update inventory (subtract for SALE, add for BUY)
if transaction_type == "SALE":
c.execute("UPDATE items SET item_quantity = item_quantity - ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
else: # BUY
c.execute("UPDATE items SET item_quantity = item_quantity + ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
# Clear cart
c.execute("DELETE FROM carts WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
emoji = "💰" if transaction_type == "SALE" else "📦"
return f"{emoji} Transaction #{transaction_id} completed: ${total:.2f}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error processing cart: {e}")
return "Error processing cart."
def clear_cart(user_id):
"""Clear user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("DELETE FROM carts WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
return "🗑️ Cart cleared."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error clearing cart: {e}")
return "Error clearing cart."
def process_inventory_command(nodeID, message, name="none"):
"""Process inventory and POS commands"""
# Check ban list
if str(nodeID) in bbs_ban_list:
logger.warning("System: Inventory attempt from the ban list")
return "Unable to process command"
message_lower = message.lower()
parts = message.split()
try:
# Help command
if "?" in message_lower:
return get_inventory_help()
# Item management commands
if message_lower.startswith("itemadd "):
# itemadd <name> <qty> [price] [location]
if len(parts) < 3:
return "Usage: itemadd <name> <qty> [price] [location]"
item_name = parts[1]
try:
quantity = int(parts[2])
except ValueError:
return "Invalid quantity."
price = 0.0
location = ""
if len(parts) > 3:
try:
price = float(parts[3])
location = " ".join(parts[4:]) if len(parts) > 4 else ""
except ValueError:
# If price is omitted, treat parts[3] as location
price = 0.0
location = " ".join(parts[3:])
return add_item(item_name, price, quantity, location)
elif message_lower.startswith("itemremove "):
item_name = " ".join(parts[1:])
return remove_item(item_name)
elif message_lower.startswith("itemreset "):
# itemreset name [price=X] [quantity=Y]
if len(parts) < 2:
return "Usage: itemreset <name> [price=X] [quantity=Y]"
item_name = parts[1]
price = None
quantity = None
for part in parts[2:]:
if part.startswith("price="):
try:
price = float(part.split("=")[1])
except ValueError:
return "Invalid price value."
elif part.startswith("quantity=") or part.startswith("qty="):
try:
quantity = int(part.split("=")[1])
except ValueError:
return "Invalid quantity value."
return reset_item(item_name, price, quantity)
elif message_lower.startswith("itemsell "):
# itemsell name quantity [notes]
if len(parts) < 3:
return "Usage: itemsell <name> <quantity> [notes]"
item_name = parts[1]
try:
quantity = int(parts[2])
notes = " ".join(parts[3:]) if len(parts) > 3 else ""
return sell_item(item_name, quantity, name, notes)
except ValueError:
return "Invalid quantity."
elif message_lower.startswith("itemreturn "):
# itemreturn transaction_id
if len(parts) < 2:
return "Usage: itemreturn <transaction_id>"
try:
transaction_id = int(parts[1])
return return_item(transaction_id)
except ValueError:
return "Invalid transaction ID."
elif message_lower.startswith("itemloan "):
# itemloan <name> <note>
if len(parts) < 3:
return "Usage: itemloan <name> <note>"
item_name = parts[1]
note = " ".join(parts[2:])
return loan_item(item_name, name, note)
elif message_lower == "itemlist":
return list_items()
elif message_lower == "itemstats":
return get_stats()
# Cart commands
elif message_lower.startswith("cartadd "):
# cartadd name quantity
if len(parts) < 3:
return "Usage: cartadd <name> <quantity>"
item_name = parts[1]
try:
quantity = int(parts[2])
return add_to_cart(str(nodeID), item_name, quantity)
except ValueError:
return "Invalid quantity."
elif message_lower.startswith("cartremove "):
item_name = " ".join(parts[1:])
return remove_from_cart(str(nodeID), item_name)
elif message_lower == "cartlist" or message_lower == "cart":
return list_cart(str(nodeID))
elif message_lower.startswith("cartbuy") or message_lower.startswith("cartsell"):
transaction_type = "BUY" if "buy" in message_lower else "SALE"
notes = " ".join(parts[1:]) if len(parts) > 1 else ""
return checkout_cart(str(nodeID), name, transaction_type, notes)
elif message_lower == "cartclear":
return clear_cart(str(nodeID))
else:
return "Invalid command. Send 'item?' for help."
except Exception as e:
logger.error(f"Inventory: Error processing command: {e}")
return "Error processing command."
def get_inventory_help():
"""Return help text for inventory commands"""
return (
"📦 Inventory Commands:\n"
" itemadd <name> <qty> [price] [loc]\n"
" itemremove <name>\n"
" itemreset name> <qty> [price] [loc]\n"
" itemsell <name> <qty> [notes]\n"
" itemloan <name> <note>\n"
" itemreturn <transaction_id>\n"
" itemlist\n"
" itemstats\n"
"\n"
"🛒 Cart Commands:\n"
" cartadd <name> <qty>\n"
" cartremove <name>\n"
" cartlist\n"
" cartbuy/cartsell [notes]\n"
" cartclear\n"
)

88
modules/llm.md Normal file
View File

@@ -0,0 +1,88 @@
# How do I use this thing?
This is not a full turnkey setup yet?
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.
# Ollama local
```bash
# bash
curl -fsSL https://ollama.com/install.sh | sh
# docker
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -e OLLAMA_API_BASE_URL=http://host.docker.internal:11434 open-webui/open-webui
```
## Update /etc/systemd/system/ollama.service
https://github.com/ollama/ollama/issues/703
```ini
#service file addition not config.ini
# [Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
```
## validation
http://IP::11434
`Ollama is running`
## Docs
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?
---
# OpenWebUI (docker)
```bash
## ollama in docker
docker run -d -p 3000:8080 --gpus all -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:cuda
## external ollama
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://IP:11434 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
wait for engine to build, update the config.ini for the bot
```ini
# Use OpenWebUI instead of direct Ollama API (enables advanced RAG features)
useOpenWebUI = False
# OpenWebUI server URL (e.g., http://localhost:3000)
openWebUIURL = http://localhost:3000
# OpenWebUI API key/token (required when useOpenWebUI is True)
openWebUIAPIKey = sk-xxxx (see below for help)
```
## Validation
http://IP:3000
make a new admin user.
validate you have models imported or that the system is working for query.
make a new user for the bot
## API Key
- upper right settings for the user
- settings -> account
- get/create the API key for the user
## Troubleshooting
- make sure the OpenWebUI works from the bot node and loads (try lynx etc)
- make sure the model in config.ini is also loaded in OpenWebUI and you can use it
- make sure **OpenWebUI** can reach **Ollama IP** it should auto import the models
- I find using IP and not common use names like localhost which may not work well with docker etc..
- Check OpenWebUI and Ollama are working
- Go to Admin Settings within Open WebUI.
- Connections tab
- Ollama connection and click on the Manage (wrench icon)
- download models directly from the Ollama library
- **Once the model is downloaded or imported, it will become available for use within Open WebUI, allowing you to interact with it through the chat interface**
## Docs
[OpenWebUI Quick Start](https://docs.openwebui.com/getting-started/quick-start/)
[OpenWebUI API](https://docs.openwebui.com/getting-started/api-endpoints)
[OpenWebUI Ollama](https://docs.openwebui.com/getting-started/quick-start/starting-with-ollama/)
[Blog OpenWebUI on Pi](https://pimylifeup.com/raspberry-pi-open-webui/)
https://docs.openwebui.com/tutorials/tips/rag-tutorial#tutorial-configuring-rag-with-open-webui-documentation
https://docs.openwebui.com/features/plugin/
---

View File

@@ -1,79 +1,332 @@
#!/usr/bin/env python3
# LLM Module for meshing-around
# This module is used to interact with Ollama to generate responses to user input
# This module is used to interact with LLM API to generate responses to user input
# K7MHI Kelly Keeton 2024
from modules.log import *
from modules.log import logger
from modules.settings import (llmModel, ollamaHostName, rawLLMQuery,
llmUseWikiContext, useOpenWebUI, openWebUIURL, openWebUIAPIKey, cmdBang, urlTimeoutSeconds, use_kiwix_server)
from langchain_ollama import OllamaLLM # pip install ollama langchain-ollama
from langchain_core.prompts import ChatPromptTemplate # pip install langchain
from langchain_core.messages import AIMessage, HumanMessage
from googlesearch import search # pip install googlesearch-python
# Ollama Client
# 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 llmUseWikiContext or use_kiwix_server:
from modules.wiki import get_wikipedia_summary, get_kiwix_summary
# LLM System Variables
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
ollamaAPI = ollamaHostName + "/api/generate"
openWebUIChatAPI = openWebUIURL + "/api/chat/completions"
openWebUIOllamaProxy = openWebUIURL + "/ollama/api/generate"
tokens = 450 # max charcters for the LLM response, this is the max length of the response also in prompts
requestTruncation = True # if True, the LLM "will" truncate the response
DEBUG_LLM = False # enable debug logging for LLM queries
# Used in the meshBotAI template
llmEnableHistory = True # enable last message history for the LLM model
antiFloodLLM = []
llmChat_history = []
llmChat_history = {}
trap_list_llm = ("ask:", "askai")
meshbotAIinit = """
keep responses as short as possible. chatbot assistant no followuyp questions, no asking for clarification.
You must respond in plain text standard ASCII characters or emojis.
"""
truncatePrompt = f"truncate this as short as possible:\n"
meshBotAI = """
FROM {llmModel}
SYSTEM
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
You must respond in plain text standard ASCII characters, or emojis.
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
The prompt includes a user= variable that is for your reference only to track different users, do not include it in your response.
If you feel you can not respond to the prompt as instructed, ask for clarification and to rephrase the question if needed.
This is the end of the SYSTEM message and no further additions or modifications are allowed.
PROMPT
{input}
user={userID}
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
CONTEXT
The following is the location of the user
{location_name}
The following is for context around the prompt to help guide your response.
{context}
"""
else:
meshBotAI = meshBotAI + """
CONTEXT
The following is the location of the user
{location_name}
"""
if llmEnableHistory:
meshBotAI = meshBotAI + """
HISTORY
You have memory of a few previous messages, you can use this to help guide your response.
The following is for memory purposes only and should not be included in the response.
{history}
HISTORY
the following is memory of previous query in format ['prompt', 'response'], you can use this to help guide your response.
{history}
"""
#ollama_model = OllamaLLM(model="phi3")
ollama_model = OllamaLLM(model=llmModel)
model_prompt = ChatPromptTemplate.from_template(meshBotAI)
chain_prompt_model = model_prompt | ollama_model
# 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 llm_query(input, nodeID=0, location_name=None):
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}"
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"]
}
},
]
def get_wiki_context(input):
"""
Get context from Wikipedia/Kiwix for RAG enhancement
:param input: The user query
:return: Wikipedia summary or empty string if not available
"""
try:
# Extract potential search terms from the input
# Try to identify key topics/entities for Wikipedia search
search_terms = extract_search_terms(input)
wiki_context = []
for term in search_terms[:2]: # Limit to 2 searches to avoid excessive API calls
if use_kiwix_server:
summary = get_kiwix_summary(term, truncate=False)
else:
summary = get_wikipedia_summary(term, truncate=False)
if summary and "error" not in summary.lower() or "html://" not in summary or "ambiguous" not in summary.lower():
wiki_context.append(f"Wikipedia context for '{term}': {summary}")
return '\n'.join(wiki_context) if wiki_context else ''
except Exception as e:
logger.debug(f"System: LLM Query: Wiki context gathering failed: {e}")
return ''
def llm_extract_topic(input):
"""
Use LLM to extract the main topic as a single word or short phrase.
Always uses raw mode and supports both Ollama and OpenWebUI.
:param input: The user query
:return: List with one topic string, or empty list on failure
"""
prompt = (
"Summarize the following query into a single word or short phrase that best represents the main topic, "
"for use as a Wikipedia search term. Only return the word or phrase, nothing else:\n"
f"{input}"
)
try:
if useOpenWebUI and openWebUIAPIKey:
result = send_openwebui_query(prompt, max_tokens=10)
else:
llmQuery = {"model": llmModel, "prompt": prompt, "stream": False, "max_tokens": 10}
result = send_ollama_query(llmQuery)
topic = result.strip().split('\n')[0]
topic = topic.strip(' "\'.,!?;:')
if topic:
return [topic]
except Exception as e:
logger.debug(f"LLM topic extraction failed: {e}")
return []
def extract_search_terms(input):
"""
Extract potential search terms from user input.
Enhanced: Try LLM-based topic extraction first, fallback to heuristic.
:param input: The user query
:return: List of potential search terms
"""
# Remove common command prefixes
for trap in trap_list_llm:
if input.lower().startswith(trap):
input = input[len(trap):].strip()
break
# Try LLM-based extraction first
terms = llm_extract_topic(input)
if terms:
return terms
# Fallback: Simple heuristic (existing code)
words = input.split()
search_terms = []
temp_phrase = []
for word in words:
clean_word = word.strip('.,!?;:')
if clean_word and clean_word[0].isupper() and len(clean_word) > 2:
temp_phrase.append(clean_word)
elif temp_phrase:
search_terms.append(' '.join(temp_phrase))
temp_phrase = []
if temp_phrase:
search_terms.append(' '.join(temp_phrase))
if not search_terms:
search_terms = [input.strip()]
if DEBUG_LLM:
logger.debug(f"Extracted search terms: {search_terms}")
return search_terms[:3] # Limit to 3 terms
def send_openwebui_query(prompt, model=None, max_tokens=450, context=''):
"""
Send query to OpenWebUI API for chat completion
:param prompt: The user prompt
:param model: Model name (optional, defaults to llmModel)
:param max_tokens: Max tokens for response
:param context: Additional context to include
:return: Response text or error message
"""
if model is None:
model = llmModel
headers = {
'Authorization': f'Bearer {openWebUIAPIKey}',
'Content-Type': 'application/json'
}
messages = []
if context:
messages.append({
"role": "system",
"content": f"Use the following context to help answer questions:\n{context}"
})
messages.append({
"role": "user",
"content": prompt
})
data = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"stream": False
}
# Debug logging
if DEBUG_LLM:
logger.debug(f"OpenWebUI payload: {json.dumps(data)}")
logger.debug(f"OpenWebUI endpoint: {openWebUIChatAPI}")
try:
result = requests.post(openWebUIChatAPI, headers=headers, json=data, timeout=urlTimeoutSeconds * 5)
if DEBUG_LLM:
logger.debug(f"OpenWebUI response status: {result.status_code}")
logger.debug(f"OpenWebUI response text: {result.text}")
if result.status_code == 200:
result_json = result.json()
# OpenWebUI returns OpenAI-compatible format
if 'choices' in result_json and len(result_json['choices']) > 0:
response = result_json['choices'][0]['message']['content']
return response.strip()
else:
logger.warning(f"System: OpenWebUI API returned unexpected format")
return "⛔️ Response Error"
else:
logger.warning(f"System: OpenWebUI API returned status code {result.status_code}")
return f"⛔️ Request Error"
except requests.exceptions.RequestException as e:
logger.warning(f"System: OpenWebUI API request failed: {e}")
return f"⛔️ Request Error"
def send_ollama_query(llmQuery):
# Send the query to the Ollama API and return the response
try:
result = requests.post(ollamaAPI, data=json.dumps(llmQuery), timeout= urlTimeoutSeconds * 5)
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:
logger.warning(f"System: LLM Query: Ollama API returned status code {result.status_code}")
return f"⛔️ Request Error"
return result
except requests.exceptions.RequestException as e:
logger.warning(f"System: LLM Query: Ollama API request failed: {e}")
return f"⛔️ Request Error"
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, init=False):
global antiFloodLLM, llmChat_history
googleResults = []
wikiContext = ''
# if this is the first initialization of the LLM the query of " " should bring meshbotAIinit OTA shouldnt reach this?
# This is for LLM like gemma and others now?
if init 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
elif init:
input = input.strip()
# classic model for gemma2, deepseek-r1, etc
logger.debug(f"System: Using SYSTEM model framework, ideally for gemma2, deepseek-r1, etc")
if not location_name:
location_name = "no location provided "
# Remove command bang if present
if cmdBang and input.startswith('!'):
input = input.strip('!').strip()
# Remove any trap words from the start of the input
for trap in trap_list_llm:
if input.lower().startswith(trap):
input = input[len(trap):].strip()
break
# add the naughty list here to stop the function before we continue
# add a list of allowed nodes only to use the function
@@ -83,69 +336,111 @@ def llm_query(input, nodeID=0, location_name=None):
else:
antiFloodLLM.append(nodeID)
if llmContext_fromGoogle:
# grab some context from the internet using google search hits (if available)
# localization details at https://pypi.org/project/googlesearch-python/
# remove common words from the search query
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
try:
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
if googleSearch:
for result in googleSearch:
# SearchResult object has url= title= description= just grab title and description
googleResults.append(f"{result.title} {result.description}")
# Get Wikipedia/Kiwix context if enabled (RAG)
if llmUseWikiContext and input != meshbotAIinit:
# get_wiki_context returns a string, but we want to count the items before joining
search_terms = extract_search_terms(input)
wiki_context_list = []
for term in search_terms[:2]:
if not use_kiwix_server:
summary = get_wiki_context(term)
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']
summary = get_wiki_context(term)
if summary and "error" not in summary.lower():
wiki_context_list.append(f"Wikipedia context for '{term}': {summary}")
wikiContext = '\n'.join(wiki_context_list) if wiki_context_list else ''
if wikiContext:
logger.debug(f"System: using Wikipedia/Kiwix context for LLM query got {len(wiki_context_list)} results")
if googleResults:
logger.debug(f"System: LLM Query: {input} From:{nodeID} with context from google")
else:
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
history = llmChat_history.get(nodeID, ["", ""])
response = ""
result = ""
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \
"history": llmChat_history, "context": googleResults, "location_name": location_name})
# Use OpenWebUI if enabled
if useOpenWebUI and openWebUIAPIKey:
logger.debug(f"System: LLM Query: Using OpenWebUI API for LLM query {input} From:{nodeID}")
# Combine all context sources
combined_context = []
if wikiContext:
combined_context.append(wikiContext)
context_str = '\n\n'.join(combined_context)
# For OpenWebUI, we send a cleaner prompt
if rawLLMQuery:
result = send_openwebui_query(input, context=context_str, max_tokens=tokens)
else:
# Use the template for non-raw queries
modelPrompt = meshBotAI.format(
input=input,
context=context_str if combined_context else 'no other context provided',
location_name=location_name,
llmModel=llmModel,
history=history
)
result = send_openwebui_query(modelPrompt, max_tokens=tokens)
else:
logger.debug(f"System: LLM Query: Using Ollama API for LLM query {input} From:{nodeID}")
# Use standard Ollama API
if rawLLMQuery:
# sanitize the input to remove tool call syntax
if '```' in input:
logger.warning("System: LLM Query: Code markdown detected, removing for raw query")
input = input.replace('```bash', '').replace('```python', '').replace('```', '')
modelPrompt = input
# Add wiki context to raw queries if available
if wikiContext:
modelPrompt = f"Context:\n{wikiContext}\n\nQuestion: {input}"
else:
# Build the query from the template
all_context = []
if wikiContext:
all_context.append(wikiContext)
context_text = '\n'.join(all_context) if all_context else 'no other context provided'
modelPrompt = meshBotAI.format(
input=input,
context=context_text,
location_name=location_name,
llmModel=llmModel,
history=history
)
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
# Query the model via Ollama web API
result = send_ollama_query(llmQuery)
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
antiFloodLLM.remove(nodeID) # Ensure removal on error
logger.warning(f"System: LLM failure: {e}")
return "I am having trouble processing your request, please try again later."
return "⛔️I am having trouble processing your request, please try again later."
# cleanup for message output
response = result.strip().replace('\n', ' ')
if rawLLMQuery and requestTruncation and len(response) > 450:
# retry loop to truncate the response
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
truncate_prompt_full = truncatePrompt + response
if useOpenWebUI and openWebUIAPIKey:
truncateResult = send_openwebui_query(truncate_prompt_full, max_tokens=tokens)
else:
truncateQuery = {"model": llmModel, "prompt": truncate_prompt_full, "stream": False, "max_tokens": tokens}
truncateResult = send_ollama_query(truncateQuery)
# Store history of the conversation, with limit to prevent template growing too large causing speed issues
if len(llmChat_history) > llm_history_limit:
# remove the oldest two messages
llmChat_history.pop(0)
llmChat_history.pop(1)
inputWithUserID = input + f" user={nodeID}"
llmChat_history.append(HumanMessage(content=inputWithUserID))
llmChat_history.append(AIMessage(content=response))
# cleanup for message output
response = truncateResult.strip().replace('\n', ' ')
# done with the query, remove the user from the anti flood list
antiFloodLLM.remove(nodeID)
return response
if llmEnableHistory:
llmChat_history[nodeID] = [input, response]
# import subprocess
# def get_ollama_cpu():
# try:
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
# if "GPU" in psOutput.stdout:
# logger.debug(f"System: Ollama process with GPU")
# else:
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False
return response

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
# Custom logger for MeshBot and PongBot
# you can change the sdtout_handler level to logging.INFO to only show INFO level logs
# stdout_handler.setLevel(logging.INFO)vs stdout_handler.setLevel(logging.DEBUG)
# 2024 Kelly Keeton K7MHI
import logging
from datetime import datetime
from modules.settings import *
from logging.handlers import TimedRotatingFileHandler
import modules.settings as my_settings
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
if not my_settings.LOGGING_LEVEL:
my_settings.LOGGING_LEVEL = "DEBUG"
LOGGING_LEVEL = getattr(logging, my_settings.LOGGING_LEVEL)
class CustomFormatter(logging.Formatter):
grey = '\x1b[38;21m'
@@ -33,10 +34,23 @@ class CustomFormatter(logging.Formatter):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
class plainFormatter(logging.Formatter):
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)
for code in self.ansi_codes:
message = message.replace(code, '')
return message
# Create logger
logger = logging.getLogger("MeshBot System Logger")
logger.setLevel(logging.DEBUG)
logger.setLevel(LOGGING_LEVEL)
logger.propagate = False
msgLogger = logging.getLogger("MeshBot Messages Logger")
@@ -46,27 +60,40 @@ 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()
# Set level for stdout handler (logs DEBUG level and above)
stdout_handler.setLevel(logging.DEBUG)
stdout_handler.setLevel(LOGGING_LEVEL)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Add handlers to the logger
logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('system{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler.setFormatter(logging.Formatter(logFormat))
logger.addHandler(file_handler)
if log_messages_to_file:
if my_settings.syslog_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=my_settings.log_backup_count, encoding='utf-8')
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
file_handler_sys.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
if my_settings.log_messages_to_file:
# Create file handler for logging to a file
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=my_settings.log_backup_count, encoding='utf-8')
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)
# Pretty Timestamp
def getPrettyTime(seconds):
# convert unix time to minutes, hours, days, or years for simple display
if seconds < 60:
return f"{int(seconds)}s"
elif seconds < 3600:
return f"{int(round(seconds / 60))}m"
elif seconds < 86400:
return f"{int(round(seconds / 3600))}h"
elif seconds < 31536000:
return f"{int(round(seconds / 86400))}d"
else:
return f"{int(round(seconds / 31536000))}y"

68
modules/qrz.py Normal file
View File

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

55
modules/radio.md Normal file
View File

@@ -0,0 +1,55 @@
# Radio Module: Meshages TTS (Text-to-Speech) Setup
The radio module supports audible mesh messages using the [KittenTTS](https://github.com/KittenML/KittenTTS) engine. This allows the bot to generate and play speech from text, making mesh alerts and messages audible on your device.
## Features
- Converts mesh messages to speech using KittenTTS.
## Installation
1. **Install Python dependencies:**
- `kittentts` is the TTS engine.
`pip install https://github.com/KittenML/KittenTTS/releases/download/0.1/kittentts-0.1.0-py3-none-any.whl`
2. **Install PortAudio (required for sounddevice):**
- **macOS:**
```sh
brew install portaudio
```
- **Linux (Debian/Ubuntu):**
```sh
sudo apt-get install portaudio19-dev
```
- **Windows:**
No extra step needed; `sounddevice` will use the default audio driver.
## Configuration
- Enable TTS in your `config.ini`:
```ini
[radioMon]
meshagesTTS = True
```
## Usage
When enabled, the bot will generate and play speech for mesh messages using the selected voice.
No additional user action is required.
## Troubleshooting
- If you see errors about missing `sounddevice` or `portaudio`, ensure you have installed the dependencies above.
- On macOS, you may need to allow microphone/audio access for your terminal.
- If you have audio issues, check your systems default output device.
## References
- [KittenTTS GitHub](https://github.com/KittenML/KittenTTS)
- [KittenTTS Model on HuggingFace](https://huggingface.co/KittenML/kitten-tts-nano-0.2)
- [sounddevice documentation](https://python-sounddevice.readthedocs.io/)
---

View File

@@ -1,19 +1,240 @@
# meshing around with hamlib as a source for info to send to mesh network
# detect signal strength and frequency of active channel if appears to be in use send to mesh network
# depends on rigctld running externally as a network service
# 2024 Kelly Keeton K7MHI
# also can use VOX detection with a microphone and vosk speech to text to send voice messages to mesh network
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
# 2025 Kelly Keeton K7MHI
# WSJT-X and JS8Call UDP Monitoring
# Based on WSJT-X UDP protocol specification
# Reference: https://github.com/ckuhtz/ham/blob/main/mcast/recv_decode.py
import socket
import asyncio
from modules.log import *
import socket
import struct
import json
from modules.log import logger
# verbose debug logging for trap words function
debugVoxTmsg = False
from modules.settings import (
radio_detection_enabled,
rigControlServerAddress,
signalDetectionThreshold,
signalHoldTime,
signalCooldown,
signalCycleLimit,
voxDetectionEnabled,
useLocalVoxModel,
localVoxModelPath,
voxLanguage,
voxInputDevice,
voxTrapList,
voxOnTrapList,
voxEnableCmd,
ERROR_FETCHING_DATA,
meshagesTTS,
)
# module global variables
previousStrength = -40
signalCycle = 0
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
}
# --- WSJT-X and JS8Call Settings Initialization ---
wsjtxMsgQueue = [] # Queue for WSJT-X detected messages
js8callMsgQueue = [] # Queue for JS8Call detected messages
wsjtx_enabled = False
js8call_enabled = False
wsjtx_udp_port = 2237
js8call_udp_port = 2442
watched_callsigns = []
wsjtx_udp_address = '127.0.0.1'
js8call_tcp_address = '127.0.0.1'
js8call_tcp_port = 2442
# WSJT-X UDP Protocol Message Types
WSJTX_HEARTBEAT = 0
WSJTX_STATUS = 1
WSJTX_DECODE = 2
WSJTX_CLEAR = 3
WSJTX_REPLY = 4
WSJTX_QSO_LOGGED = 5
WSJTX_CLOSE = 6
WSJTX_REPLAY = 7
WSJTX_HALT_TX = 8
WSJTX_FREE_TEXT = 9
WSJTX_WSPR_DECODE = 10
WSJTX_LOCATION = 11
WSJTX_LOGGED_ADIF = 12
try:
from modules.settings import (
wsjtx_detection_enabled,
wsjtx_udp_server_address,
wsjtx_watched_callsigns,
js8call_detection_enabled,
js8call_server_address,
js8call_watched_callsigns
)
wsjtx_enabled = wsjtx_detection_enabled
js8call_enabled = js8call_detection_enabled
# Use a local list to collect callsigns before assigning to watched_callsigns
callsigns = []
if wsjtx_enabled:
if ':' in wsjtx_udp_server_address:
wsjtx_udp_address, port_str = wsjtx_udp_server_address.split(':')
wsjtx_udp_port = int(port_str)
if wsjtx_watched_callsigns:
callsigns.extend([cs.strip() for cs in wsjtx_watched_callsigns.split(',') if cs.strip()])
if js8call_enabled:
if ':' in js8call_server_address:
js8call_tcp_address, port_str = js8call_server_address.split(':')
js8call_tcp_port = int(port_str)
if js8call_watched_callsigns:
callsigns.extend([cs.strip() for cs in js8call_watched_callsigns.split(',') if cs.strip()])
# Clean up and deduplicate callsigns, uppercase for matching
watched_callsigns = list({cs.upper() for cs in callsigns})
except ImportError:
logger.debug("System: RadioMon: WSJT-X/JS8Call settings not configured")
except Exception as e:
logger.warning(f"System: RadioMon: Error loading WSJT-X/JS8Call settings: {e}")
if radio_detection_enabled:
# used by hamlib detection
import socket
if voxDetectionEnabled:
# methods available for trap word processing, these can be called by VOX detection when trap words are detected
from mesh_bot import tell_joke, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
botMethods = {
"joke": tell_joke,
"weather": handle_wxc,
"moon": handle_moon,
"daylight": handle_sun,
"river": handle_riverFlow,
"tide": handle_tide,
"satellite": handle_satpass}
# module global variables
previousVoxState = False
voxHoldTime = signalHoldTime
try:
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
from vosk import Model, KaldiRecognizer # pip install vosk
import json
q = asyncio.Queue(maxsize=32) # queue for audio data
if useLocalVoxModel:
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
else:
voxModel = Model(lang=voxLanguage) # use built in model for specified language
except Exception as e:
print(f"System: RadioMon: Error importing VOX dependencies: {e}")
print(f"To use VOX detection please install the vosk and sounddevice python modules")
print(f"pip install vosk sounddevice")
print(f"sounddevice needs pulseaudio, apt-get install portaudio19-dev")
voxDetectionEnabled = False
logger.error(f"System: RadioMon: VOX detection disabled due to import error")
if meshagesTTS:
try:
# TTS for meshages imports
logger.debug("System: RadioMon: Initializing TTS model for audible meshages")
import sounddevice as sd
from kittentts import KittenTTS
ttsModel = KittenTTS("KittenML/kitten-tts-nano-0.2")
available_voices = [
'expr-voice-2-m', 'expr-voice-2-f', 'expr-voice-3-m', 'expr-voice-3-f',
'expr-voice-4-m', 'expr-voice-4-f', 'expr-voice-5-m', 'expr-voice-5-f'
]
except Exception as e:
logger.error(f"To use Meshages TTS please review the radio.md documentation for setup instructions.")
meshagesTTS = False
async def generate_and_play_tts(text, voice, samplerate=24000):
"""Async: Generate speech and play audio."""
text = text.strip()
if not text:
return
try:
logger.debug(f"System: RadioMon: Generating TTS for text: {text} with voice: {voice}")
audio = await asyncio.to_thread(ttsModel.generate, text, voice=voice)
if audio is None or len(audio) == 0:
return
await asyncio.to_thread(sd.play, audio, samplerate)
await asyncio.to_thread(sd.wait)
del audio
except Exception as e:
logger.warning(f"System: RadioMon: Error in generate_and_play_tts: {e}")
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
if "socket" not in globals():
logger.warning("System: RadioMon: 'socket' module not imported. Hamlib disabled.")
return ERROR_FETCHING_DATA
try:
rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
rigControlSocket.settimeout(2)
rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1])))
except Exception as e:
logger.error(f"RadioMon: Error connecting to rigctld: {e}")
logger.error(f"System: RadioMon: Error connecting to rigctld: {e}")
return ERROR_FETCHING_DATA
try:
@@ -27,112 +248,49 @@ def get_hamlib(msg="f"):
data = data.replace(b'\n',b'')
return data.decode("utf-8").rstrip()
except Exception as e:
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
logger.error(f"System: RadioMon: Error fetching data from rigctld: {e}")
return ERROR_FETCHING_DATA
def get_freq_common_name(freq):
freq = int(freq)
if freq == 462562500:
return "GRMS CH1"
elif freq == 462587500:
return "GRMS CH2"
elif freq == 462612500:
return "GRMS CH3"
elif freq == 462637500:
return "GRMS CH4"
elif freq == 462662500:
return "GRMS CH5"
elif freq == 462687500:
return "GRMS CH6"
elif freq == 462712500:
return "GRMS CH7"
elif freq == 467562500:
return "GRMS CH8"
elif freq == 467587500:
return "GRMS CH9"
elif freq == 467612500:
return "GRMS CH10"
elif freq == 467637500:
return "GRMS CH11"
elif freq == 467662500:
return "GRMS CH12"
elif freq == 467687500:
return "GRMS CH13"
elif freq == 467712500:
return "GRMS CH14"
elif freq == 467737500:
return "GRMS CH15"
elif freq == 462550000:
return "GRMS CH16"
elif freq == 462575000:
return "GMRS CH17"
elif freq == 462600000:
return "GMRS CH18"
elif freq == 462625000:
return "GMRS CH19"
elif freq == 462675000:
return "GMRS CH20"
elif freq == 462670000:
return "GMRS CH21"
elif freq == 462725000:
return "GMRS CH22"
elif freq == 462725500:
return "GMRS CH23"
elif freq == 467575000:
return "GMRS CH24"
elif freq == 467600000:
return "GMRS CH25"
elif freq == 467625000:
return "GMRS CH26"
elif freq == 467650000:
return "GMRS CH27"
elif freq == 467675000:
return "GMRS CH28"
elif freq == 467700000:
return "FRS CH1"
elif freq == 462575000:
return "FRS CH2"
elif freq == 462600000:
return "FRS CH3"
elif freq == 462650000:
return "FRS CH5"
elif freq == 462675000:
return "FRS CH6"
elif freq == 462700000:
return "FRS CH7"
elif freq == 462725000:
return "FRS CH8"
elif freq == 462562500:
return "FRS CH9"
elif freq == 462587500:
return "FRS CH10"
elif freq == 462612500:
return "FRS CH11"
elif freq == 462637500:
return "FRS CH12"
elif freq == 462662500:
return "FRS CH13"
elif freq == 462687500:
return "FRS CH14"
elif freq == 462712500:
return "FRS CH15"
elif freq == 462737500:
return "FRS CH16"
elif freq == 146520000:
return "2M Simplex Calling"
elif freq == 446000000:
return "70cm Simplex Calling"
elif freq == 156800000:
return "Marine CH16"
else:
#return Mhz
freq = freq/1000000
return f"{freq} Mhz"
def get_sig_strength():
strength = get_hamlib('l STRENGTH')
return strength
def checkVoxTrapWords(text):
try:
if not voxOnTrapList:
logger.debug(f"System: 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"System: 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"System: 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"System: 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"System: RadioMon: VOX returning text after trap word '{trap_lower}': '{new_text}'")
return new_text
if debugVoxTmsg:
logger.debug(f"System: RadioMon: VOX no trap word found in: '{text}'")
return None
except Exception as e:
logger.debug(f"System: RadioMon: Error in checkVoxTrapWords: {e}")
return None
async def signalWatcher():
global previousStrength
global signalCycle
@@ -140,7 +298,7 @@ async def signalWatcher():
signalStrength = int(get_sig_strength())
if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold:
message = f"Detected {get_freq_common_name(get_hamlib('f'))} active. S-Meter:{signalStrength}dBm"
logger.debug(f"RadioMon: {message}. Waiting for {signalHoldTime} seconds")
logger.debug(f"System: RadioMon: {message}. Waiting for {signalHoldTime} seconds")
previousStrength = signalStrength
signalCycle = 0
await asyncio.sleep(signalHoldTime)
@@ -157,4 +315,322 @@ async def signalWatcher():
signalCycle = 0
previousStrength = -40
# end of file
async def make_vox_callback(loop, q):
def vox_callback(indata, frames, time, status):
if status:
logger.warning(f"System: 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("System: RadioMon: VOX queue full, dropping audio frame")
except RuntimeError:
# Loop may be closed
pass
return vox_callback
async def voxMonitor():
global previousVoxState, voxMsgQueue
try:
model = voxModel
device_info = sd.query_devices(voxInputDevice, 'input')
samplerate = 16000
logger.debug(f"System: RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
rec = KaldiRecognizer(model, samplerate)
loop = asyncio.get_running_loop()
callback = await make_vox_callback(loop, q)
with sd.RawInputStream(
device=voxInputDevice,
samplerate=samplerate,
blocksize=4000,
dtype='int16',
channels=1,
callback=callback
):
while True:
data = await q.get()
if rec.AcceptWaveform(data):
result = rec.Result()
text = json.loads(result).get("text", "")
# process text
if text and text != 'huh':
result = checkVoxTrapWords(text)
if result:
# If result is a function return, handle it (send to mesh, log, etc.)
# If it's just text, handle as a normal message
voxMsgQueue.append(result)
await asyncio.sleep(0.1)
except Exception as e:
logger.warning(f"System: RadioMon: Error in VOX monitor: {e}")
def decode_wsjtx_packet(data):
"""Decode WSJT-X UDP packet according to the protocol specification"""
try:
# WSJT-X uses Qt's QDataStream format (big-endian)
magic = struct.unpack('>I', data[0:4])[0]
if magic != 0xADBCCBDA:
return None
schema_version = struct.unpack('>I', data[4:8])[0]
msg_type = struct.unpack('>I', data[8:12])[0]
offset = 12
# Helper to read Qt QString (4-byte length + UTF-8 data)
def read_qstring(data, offset):
if offset + 4 > len(data):
return "", offset
length = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
if length == 0xFFFFFFFF: # Null string
return "", offset
if offset + length > len(data):
return "", offset
text = data[offset:offset+length].decode('utf-8', errors='ignore')
return text, offset + length
# Decode DECODE message (type 2)
if msg_type == WSJTX_DECODE:
# Read fields according to WSJT-X protocol
wsjtx_id, offset = read_qstring(data, offset)
# Read other decode fields: new, time, snr, delta_time, delta_frequency, mode, message
if offset + 1 > len(data):
return None
new = struct.unpack('>?', data[offset:offset+1])[0]
offset += 1
if offset + 4 > len(data):
return None
time_val = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
if offset + 4 > len(data):
return None
snr = struct.unpack('>i', data[offset:offset+4])[0]
offset += 4
if offset + 8 > len(data):
return None
delta_time = struct.unpack('>d', data[offset:offset+8])[0]
offset += 8
if offset + 4 > len(data):
return None
delta_frequency = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
mode, offset = read_qstring(data, offset)
message, offset = read_qstring(data, offset)
return {
'type': 'decode',
'id': wsjtx_id,
'new': new,
'time': time_val,
'snr': snr,
'delta_time': delta_time,
'delta_frequency': delta_frequency,
'mode': mode,
'message': message
}
# Decode QSO_LOGGED message (type 5)
elif msg_type == WSJTX_QSO_LOGGED:
wsjtx_id, offset = read_qstring(data, offset)
# Read QSO logged fields
if offset + 8 > len(data):
return None
date_off = struct.unpack('>Q', data[offset:offset+8])[0]
offset += 8
if offset + 8 > len(data):
return None
time_off = struct.unpack('>Q', data[offset:offset+8])[0]
offset += 8
dx_call, offset = read_qstring(data, offset)
dx_grid, offset = read_qstring(data, offset)
return {
'type': 'qso_logged',
'id': wsjtx_id,
'dx_call': dx_call,
'dx_grid': dx_grid
}
return None
except Exception as e:
logger.debug(f"System: RadioMon: Error decoding WSJT-X packet: {e}")
return None
def check_callsign_match(message, callsigns):
"""Check if any watched callsign appears in the message
Uses word boundary matching to avoid false positives like matching
'K7' when looking for 'K7MHI'. Callsigns are expected to be
separated by spaces or be at the start/end of the message.
"""
if not callsigns:
return True # If no filter, accept all
message_upper = message.upper()
# Split message into words for exact matching
words = message_upper.split()
for callsign in callsigns:
callsign_upper = callsign.upper()
# Pre-compute patterns for portable/mobile suffixes
callsign_with_slash = callsign_upper + '/'
callsign_with_dash = callsign_upper + '-'
slash_callsign = '/' + callsign_upper
dash_callsign = '-' + callsign_upper
# Check if callsign appears as a complete word
if callsign_upper in words:
return True
# Check for callsigns in compound forms like "K7MHI/P" or "K7MHI-7"
for word in words:
if (word.startswith(callsign_with_slash) or
word.startswith(callsign_with_dash) or
word.endswith(slash_callsign) or
word.endswith(dash_callsign)):
return True
return False
async def wsjtxMonitor():
"""Monitor WSJT-X UDP broadcasts for decode messages"""
if not wsjtx_enabled:
logger.warning("System: RadioMon: WSJT-X monitoring called but not enabled")
return
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((wsjtx_udp_address, wsjtx_udp_port))
sock.setblocking(False)
logger.info(f"System: RadioMon: WSJT-X UDP listener started on {wsjtx_udp_address}:{wsjtx_udp_port}")
if watched_callsigns:
logger.info(f"System: RadioMon: Watching for callsigns: {', '.join(watched_callsigns)}")
while True:
try:
data, addr = sock.recvfrom(4096)
decoded = decode_wsjtx_packet(data)
if decoded and decoded['type'] == 'decode':
message = decoded['message']
mode = decoded['mode']
snr = decoded['snr']
# Check if message contains watched callsigns
if check_callsign_match(message, watched_callsigns):
msg_text = f"WSJT-X {mode}: {message} (SNR: {snr:+d}dB)"
logger.info(f"System: RadioMon: {msg_text}")
wsjtxMsgQueue.append(msg_text)
except BlockingIOError:
# No data available
await asyncio.sleep(0.1)
except Exception as e:
logger.debug(f"System: RadioMon: Error in WSJT-X monitor loop: {e}")
await asyncio.sleep(1)
except Exception as e:
logger.warning(f"System: RadioMon: Error starting WSJT-X monitor: {e}")
async def js8callMonitor():
"""Monitor JS8Call TCP API for messages"""
if not js8call_enabled:
logger.warning("System: RadioMon: JS8Call monitoring called but not enabled")
return
try:
logger.info(f"System: RadioMon: JS8Call TCP listener connecting to {js8call_tcp_address}:{js8call_tcp_port}")
if watched_callsigns:
logger.info(f"System: RadioMon: Watching for callsigns: {', '.join(watched_callsigns)}")
while True:
try:
# Connect to JS8Call TCP API
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((js8call_tcp_address, js8call_tcp_port))
sock.setblocking(False)
logger.info("System: RadioMon: Connected to JS8Call API")
buffer = ""
while True:
try:
data = sock.recv(4096)
if not data:
logger.warning("System: RadioMon: JS8Call connection closed")
break
buffer += data.decode('utf-8', errors='ignore')
# Process complete JSON messages (newline delimited)
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
if not line.strip():
continue
try:
msg = json.loads(line)
msg_type = msg.get('type', '')
# Handle RX.DIRECTED and RX.ACTIVITY messages
if msg_type in ['RX.DIRECTED', 'RX.ACTIVITY']:
params = msg.get('params', {})
text = params.get('TEXT', '')
from_call = params.get('FROM', '')
snr = params.get('SNR', 0)
if text and check_callsign_match(text, watched_callsigns):
msg_text = f"JS8Call from {from_call}: {text} (SNR: {snr:+d}dB)"
logger.info(f"System: RadioMon: {msg_text}")
js8callMsgQueue.append(msg_text)
except json.JSONDecodeError:
logger.debug(f"System: RadioMon: Invalid JSON from JS8Call: {line[:100]}")
except Exception as e:
logger.debug(f"System: RadioMon: Error processing JS8Call message: {e}")
except BlockingIOError:
await asyncio.sleep(0.1)
except socket.timeout:
await asyncio.sleep(0.1)
except Exception as e:
logger.debug(f"System: RadioMon: Error in JS8Call receive loop: {e}")
break
sock.close()
logger.warning("System: RadioMon: JS8Call connection lost, reconnecting in 5s...")
await asyncio.sleep(5)
except socket.timeout:
logger.warning("System: RadioMon: JS8Call connection timeout, retrying in 5s...")
await asyncio.sleep(5)
except Exception as e:
logger.warning(f"System: RadioMon: Error connecting to JS8Call: {e}")
await asyncio.sleep(10)
except Exception as e:
logger.warning(f"System: RadioMon: Error starting JS8Call monitor: {e}")
# end of file

178
modules/rss.py Normal file
View File

@@ -0,0 +1,178 @@
# rss feed module for meshing-around 2025
from modules.log import logger
from modules.settings import rssFeedURL, rssFeedNames, rssMaxItems, rssTruncate, urlTimeoutSeconds, ERROR_FETCHING_DATA, newsAPI_KEY, newsAPIsort
import urllib.request
import xml.etree.ElementTree as ET
import html
from html.parser import HTMLParser
import bs4 as bs
import requests
import datetime
# Common User-Agent for all RSS requests
COMMON_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
class MLStripper(HTMLParser):
def __init__(self):
super().__init__()
self.reset()
self.fed = []
def handle_data(self, d):
self.fed.append(d)
def get_data(self):
return ''.join(self.fed)
def strip_tags(html_text):
# 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
RSS_RETURN_COUNT = rssMaxItems
RSS_TRIM_LENGTH = rssTruncate
def get_rss_feed(msg):
# Determine which feed to use
feed_name = ""
msg_lower = msg.lower() if msg else ""
if msg_lower and any(name.lower() in msg_lower for name in RSS_FEED_NAMES):
for name in RSS_FEED_NAMES:
if name.lower() in msg_lower:
feed_name = name
break
else:
logger.debug(f"RSS: No feed name found in message '{msg}'. Using default feed.")
feed_name = RSS_FEED_NAMES[0] if RSS_FEED_NAMES else "default"
try:
idx = RSS_FEED_NAMES.index(feed_name)
feed_url = RSS_FEED_URLS[idx]
except (ValueError, IndexError):
logger.warning(f"RSS: Feed '{feed_name}' not found in RSS_FEED_URLS ({RSS_FEED_URLS}).")
return f"Feed '{feed_name}' not found."
if "?" in msg_lower:
return f"Fetches the latest {RSS_RETURN_COUNT} entries RSS feeds. Available feeds are: {', '.join(RSS_FEED_NAMES)}. To fetch a specific feed, include its name in your request."
# Fetch and parse the RSS feed
try:
logger.debug(f"Fetching RSS feed from {feed_url} from message '{msg}'")
agent = {'User-Agent': COMMON_USER_AGENT}
request = urllib.request.Request(feed_url, headers=agent)
with urllib.request.urlopen(request, timeout=urlTimeoutSeconds) as response:
xml_data = response.read()
root = ET.fromstring(xml_data)
# Find all <item> (RSS) and <entry> (Atom) elements, regardless of namespace
items = []
for elem in root.iter():
if elem.tag.endswith('item') or elem.tag.endswith('entry'):
items.append(elem)
items = items[:RSS_RETURN_COUNT]
if not items:
logger.debug(f"No RSS or Atom feed entries found in feed xml_data: {xml_data[:500]}...")
return "No RSS or Atom feed entries found."
formatted_entries = []
seen_first3 = set() # Track first 3 words (lowercased) to avoid duplicates
for item in items:
# Helper to try multiple tag names
def find_any(item, tags):
for tag in tags:
val = item.findtext(tag)
if val:
return val
return None
title = find_any(item, [
'title',
'{http://purl.org/rss/1.0/}title',
'{http://www.w3.org/2005/Atom}title'
])
# Atom links are often attributes, not text
link = find_any(item, [
'link',
'{http://purl.org/rss/1.0/}link',
'{http://www.w3.org/2005/Atom}link'
])
if not link:
link_elem = item.find('{http://www.w3.org/2005/Atom}link')
if link_elem is not None and 'href' in link_elem.attrib:
link = link_elem.attrib['href']
description = find_any(item, [
'description',
'{http://purl.org/rss/1.0/}description',
'{http://purl.org/rss/1.0/modules/content/}encoded',
'{http://www.w3.org/2005/Atom}summary',
'{http://www.w3.org/2005/Atom}content'
])
pub_date = find_any(item, [
'pubDate',
'{http://purl.org/dc/elements/1.1/}date',
'{http://www.w3.org/2005/Atom}updated'
])
# Unescape HTML entities and strip tags
description = html.unescape(description) if description else ""
description = strip_tags(description)
if len(description) > RSS_TRIM_LENGTH:
description = description[:RSS_TRIM_LENGTH - 3] + "..."
# Duplicate check: use first 3 words of description (or title if description is empty)
text_for_dupe = description if description else (title or "")
first3 = " ".join(text_for_dupe.lower().split()[:3])
if first3 in seen_first3:
continue
seen_first3.add(first3)
formatted_entries.append(f"{title}\n{description}\n")
return "\n".join(formatted_entries)
except Exception as e:
logger.error(f"Error fetching RSS feed from {feed_url}: {e}")
return ERROR_FETCHING_DATA
def get_newsAPI(user_search="meshtastic"):
# Fetch news from NewsAPI.org
user_search = user_search.strip()
if user_search.lower().startswith("latest"):
user_search = user_search[6:].strip()
if not user_search:
user_search = "meshtastic"
try:
last_week = datetime.datetime.now() - datetime.timedelta(days=7)
newsAPIurl = (
f"https://newsapi.org/v2/everything?"
f"q={user_search}&language=en&from={last_week.strftime('%Y-%m-%d')}&sortBy={newsAPIsort}shedAt&pageSize=5&apiKey={newsAPI_KEY}"
)
response = requests.get(newsAPIurl, headers={"User-Agent": COMMON_USER_AGENT}, timeout=urlTimeoutSeconds)
news_data = response.json()
if news_data.get("status") != "ok":
error_message = news_data.get("message", "Unknown error")
logger.error(f"NewsAPI error: {error_message}")
return ERROR_FETCHING_DATA
logger.debug(f"System: NewsAPI Searching for '{user_search}' got {news_data.get('totalResults', 0)} results")
articles = news_data.get("articles", [])[:3]
news_list = []
for article in articles:
title = article.get("title", "No Title")
url = article.get("url", "")
description = article.get("description", '')
news_list.append(f"📰{title}\n{description}")
# Make a nice newspaper style output
msg = f"🗞️:"
for item in news_list:
msg += item + "\n\n"
return msg.strip()
except Exception as e:
logger.error(f"System: NewsAPI fetching news: {e}")
return ERROR_FETCHING_DATA

191
modules/scheduler.py Normal file
View File

@@ -0,0 +1,191 @@
# modules/scheduler.py 2025 meshing-around
# Scheduler module for mesh_bot
import asyncio
import schedule
from datetime import datetime
from modules.log import logger
from modules.system import send_message
async def run_scheduler_loop(interval=1):
logger.debug(f"System: Scheduler loop started Tasks: {len(schedule.jobs)}, Details:{extract_schedule_fields(schedule.get_jobs())}")
try:
last_logged_minute = -1
while True:
try:
# Log scheduled jobs every 20 minutes
now = datetime.now()
if now.minute % 20 == 0 and now.minute != last_logged_minute:
logger.debug(f"System: Scheduled Tasks {len(schedule.jobs)}, Details:{extract_schedule_fields(schedule.get_jobs())}")
last_logged_minute = now.minute
schedule.run_pending()
except Exception as e:
logger.error(f"System: Scheduler loop exception: {e}")
await asyncio.sleep(interval)
except asyncio.CancelledError:
logger.debug("System: Scheduler loop cancelled, shutting down.")
def safe_int(val, default=0, type=""):
try:
return int(val)
except (ValueError, TypeError):
logger.debug(f"System: Scheduler config {type} error '{val}' to int, using default {default}")
return default
def extract_schedule_fields(jobs):
"""
Extracts 'Every ... (last run: [...], next run: ...)' from schedule.get_jobs() output without regex.
"""
jobs_str = str(jobs)
results = []
# Split by '), ' to separate jobs, then add ')' back except last
parts = jobs_str.split('), ')
for i, part in enumerate(parts):
if not part.endswith(')'):
part += ')'
# Find the start of 'Every'
start = part.find('Every')
if start != -1:
# Find the start of 'do <lambda>()'
do_idx = part.find('do ')
if do_idx != -1:
summary = part[start:do_idx].strip()
# Find the (last run: ... next run: ...) part
paren_idx = part.find('(', do_idx)
if paren_idx != -1:
summary += ' ' + part[paren_idx:].strip()
while '<function ' in summary:
f_start = summary.find('<function ')
f_end = summary.find('>', f_start)
if f_end == -1:
break
func_str = summary[f_start+10:f_end]
func_name = func_str.split(' ')[0]
summary = summary[:f_start] + func_name + summary[f_end+1:]
results.append(summary)
return results
def setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval):
try:
# Methods imported from mesh_bot for scheduling tasks
from mesh_bot import (
tell_joke,
welcome_message,
handle_wxc,
handle_moon,
handle_sun,
handle_riverFlow,
handle_tide,
handle_satpass,
handleNews,
handle_mwx,
sysinfo,
)
from modules.rss import get_rss_feed
except ImportError as e:
logger.warning(f"Some mesh_bot schedule features are unavailable by option disable in config.ini: {e} comment out the use of these methods in your custom_scheduler.py")
# Setup the scheduler based on configuration
schedulerValue = schedulerValue.lower().strip()
schedulerTime = schedulerTime.strip()
schedulerInterval = schedulerInterval.strip()
schedulerChannel = safe_int(schedulerChannel, 0, type="channel")
schedulerInterface = safe_int(schedulerInterface, 1, type="interface")
schedulerIntervalInt = safe_int(schedulerInterval, 5, type="interval")
try:
scheduler_message = MOTD if schedulerMotd else schedulerMessage
def send_sched_msg():
send_message(scheduler_message, schedulerChannel, 0, schedulerInterface)
# Basic Scheduler Options
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
if any(option in schedulerValue for option in basicOptions):
if schedulerValue == 'day':
if schedulerTime:
# Specific time each day
schedule.every().day.at(schedulerTime).do(send_sched_msg)
else:
# Every N days
schedule.every(schedulerIntervalInt).days.do(send_sched_msg)
elif 'mon' in schedulerValue and schedulerTime:
schedule.every().monday.at(schedulerTime).do(send_sched_msg)
elif 'tue' in schedulerValue and schedulerTime:
schedule.every().tuesday.at(schedulerTime).do(send_sched_msg)
elif 'wed' in schedulerValue and schedulerTime:
schedule.every().wednesday.at(schedulerTime).do(send_sched_msg)
elif 'thu' in schedulerValue and schedulerTime:
schedule.every().thursday.at(schedulerTime).do(send_sched_msg)
elif 'fri' in schedulerValue and schedulerTime:
schedule.every().friday.at(schedulerTime).do(send_sched_msg)
elif 'sat' in schedulerValue and schedulerTime:
schedule.every().saturday.at(schedulerTime).do(send_sched_msg)
elif 'sun' in schedulerValue and schedulerTime:
schedule.every().sunday.at(schedulerTime).do(send_sched_msg)
elif 'hour' in schedulerValue:
schedule.every(schedulerIntervalInt).hours.do(send_sched_msg)
elif 'min' in schedulerValue:
schedule.every(schedulerIntervalInt).minutes.do(send_sched_msg)
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerIntervalInt} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'joke' in schedulerValue:
schedule.every(schedulerIntervalInt).minutes.do(
lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerIntervalInt} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'link' in schedulerValue:
schedule.every(schedulerIntervalInt).hours.do(
lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the link scheduler to send link messages every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'weather' in schedulerValue:
schedule.every().day.at(schedulerTime).do(
lambda: send_message(handle_wxc(0, schedulerInterface, 'wx', days=1), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'news' in schedulerValue:
schedule.every(schedulerIntervalInt).hours.do(
lambda: send_message(handleNews(0, schedulerInterface, 'readnews', False), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the news scheduler to send news updates every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'readrss' in schedulerValue:
schedule.every(schedulerIntervalInt).hours.do(
lambda: send_message(get_rss_feed(''), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the RSS scheduler to send RSS feeds every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'mwx' in schedulerValue:
schedule.every().day.at(schedulerTime).do(
lambda: send_message(handle_mwx(0, schedulerInterface, 'mwx'), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the marine weather scheduler to send marine weather updates at {schedulerTime} on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'sysinfo' in schedulerValue:
schedule.every(schedulerIntervalInt).hours.do(
lambda: send_message(sysinfo('', 0, schedulerInterface, False), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the sysinfo scheduler to send system information every {schedulerIntervalInt} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'tide' in schedulerValue:
schedule.every().day.at(schedulerTime).do(
lambda: send_message(handle_tide(0, schedulerInterface, schedulerChannel), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the tide scheduler to send tide information at {schedulerTime} on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'solar' in schedulerValue:
schedule.every().day.at(schedulerTime).do(
lambda: send_message(handle_sun(0, schedulerInterface, schedulerChannel), schedulerChannel, 0, schedulerInterface)
)
logger.debug(f"System: Starting the scheduler to send solar information at {schedulerTime} on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'custom' in schedulerValue:
try:
from modules.custom_scheduler import setup_custom_schedules # type: ignore
setup_custom_schedules(
send_message, tell_joke, welcome_message, handle_wxc, MOTD,
schedulerChannel, schedulerInterface)
logger.debug(f"System: Starting the custom_scheduler.py ")
schedule.every().monday.at("12:00").do(
lambda: logger.info("System: Scheduled Broadcast Enabled Reminder")
)
except Exception as e:
logger.warning("Custom scheduler file not found or failed to import. cp etc/custom_scheduler.template modules/custom_scheduler.py")
except Exception as e:
logger.error(f"System: Scheduler Error {e}")
return True

View File

@@ -5,9 +5,10 @@ import configparser
# messages
NO_DATA_NOGPS = "No location data: does your device have GPS?"
ERROR_FETCHING_DATA = "error fetching data"
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd? for more'
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, CMD? for more'
EMERGENCY_RESPONSE = "MeshBot detected a possible request for Emergency Assistance and alerted a wider audience."
MOTD = 'Thanks for using MeshBOT! Have a good day!'
NO_ALERTS = "No weather alerts found."
NO_ALERTS = "No alerts found."
# setup the global variables
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
@@ -19,65 +20,124 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 4 # default retry count for interfaces
retry_int1 = False
retry_int2 = False
scheduler_enabled = False # enable the scheduler currently config via code only
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
cmdHistory = [] # list to hold the command history for lheard and history commands
msg_history = [] # list to hold the message history for the messages command
max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content
voxMsgQueue = [] # queue for VOX detected messages
tts_read_queue = [] # queue for TTS messages
wsjtxMsgQueue = [] # queue for WSJT-X detected messages
js8callMsgQueue = [] # queue for JS8Call 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
# Memory Management Constants
MAX_MSG_HISTORY = 250
MAX_CMD_HISTORY = 250
MAX_SEEN_NODES = 1000
CLEANUP_INTERVAL = 86400 # 24 hours in seconds
GAMEDELAY = 3 * CLEANUP_INTERVAL # 3 days in seconds
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
config_file = "config.ini"
try:
config.read(config_file)
config.read(config_file, encoding='utf-8')
except Exception as e:
print(f"System: Error reading config file: {e}")
# exit if we can't read the config file
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")
exit(1)
if config.sections() == []:
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD,
'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
config.write(open(config_file, 'w'))
print (f"System: Config file created, check {config_file} or review the config.template")
if 'sentry' not in config:
config['Sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
config.write(open(config_file, 'w'))
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
config['bbs'] = {'enabled': 'False', 'bbsdb': 'bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
if 'repeater' not in config:
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
if 'radioMon' not in config:
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
if 'games' not in config:
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
if 'messagingSettings' not in config:
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
if 'fileMon' not in config:
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
if 'scheduler' not in config:
config['scheduler'] = {'enabled': 'False'}
config.write(open(config_file, 'w'))
if 'emergencyHandler' not in config:
config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''}
config.write(open(config_file, 'w'))
if 'smtp' not in config:
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
config.write(open(config_file, 'w'))
if 'checklist' not in config:
config['checklist'] = {'enabled': 'False', 'checklist_db': 'data/checklist.db'}
config.write(open(config_file, 'w'))
if 'qrz' not in config:
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
config.write(open(config_file, 'w'))
if 'inventory' not in config:
config['inventory'] = {'enabled': 'False', 'inventory_db': 'data/inventory.db', 'disable_penny': 'False'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
hostname1 = config['interface'].get('hostname', '')
mac1 = config['interface'].get('mac', '')
interface1_enabled = True # gotta have at least one interface
# interface2 settings
if 'interface2' in config:
@@ -89,77 +149,353 @@ if 'interface2' in config:
else:
interface2_enabled = False
# variables
# interface3 settings
if 'interface3' in config:
interface3_type = config['interface3'].get('type', 'serial')
port3 = config['interface3'].get('port', '')
hostname3 = config['interface3'].get('hostname', '')
mac3 = config['interface3'].get('mac', '')
interface3_enabled = config['interface3'].getboolean('enabled', False)
else:
interface3_enabled = False
# interface4 settings
if 'interface4' in config:
interface4_type = config['interface4'].get('type', 'serial')
port4 = config['interface4'].get('port', '')
hostname4 = config['interface4'].get('hostname', '')
mac4 = config['interface4'].get('mac', '')
interface4_enabled = config['interface4'].getboolean('enabled', False)
else:
interface4_enabled = False
# interface5 settings
if 'interface5' in config:
interface5_type = config['interface5'].get('type', 'serial')
port5 = config['interface5'].get('port', '')
hostname5 = config['interface5'].get('hostname', '')
mac5 = config['interface5'].get('mac', '')
interface5_enabled = config['interface5'].getboolean('enabled', False)
else:
interface5_enabled = False
# interface6 settings
if 'interface6' in config:
interface6_type = config['interface6'].get('type', 'serial')
port6 = config['interface6'].get('port', '')
hostname6 = config['interface6'].get('hostname', '')
mac6 = config['interface6'].get('mac', '')
interface6_enabled = config['interface6'].getboolean('enabled', False)
else:
interface6_enabled = False
# interface7 settings
if 'interface7' in config:
interface7_type = config['interface7'].get('type', 'serial')
port7 = config['interface7'].get('port', '')
hostname7 = config['interface7'].get('hostname', '')
mac7 = config['interface7'].get('mac', '')
interface7_enabled = config['interface7'].getboolean('enabled', False)
else:
interface7_enabled = False
# interface8 settings
if 'interface8' in config:
interface8_type = config['interface8'].get('type', 'serial')
port8 = config['interface8'].get('port', '')
hostname8 = config['interface8'].get('hostname', '')
mac8 = config['interface8'].get('mac', '')
interface8_enabled = config['interface8'].getboolean('enabled', False)
else:
interface8_enabled = False
# interface9 settings
if 'interface9' in config:
interface9_type = config['interface9'].get('type', 'serial')
port9 = config['interface9'].get('port', '')
hostname9 = config['interface9'].get('hostname', '')
mac9 = config['interface9'].get('mac', '')
interface9_enabled = config['interface9'].getboolean('enabled', False)
else:
interface9_enabled = False
multiple_interface = False
if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled:
multiple_interface = True
# variables from the config.ini file
try:
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
cmdBang = config['general'].getboolean('cmdBang', False) # default off
explicitCmd = config['general'].getboolean('explicitCmd', True) # default on
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
syslog_to_file = config['general'].getboolean('SyslogToFile', False)
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
urlTimeoutSeconds = config['general'].getint('urlTimeout', 15) # default 15 seconds for URL fetch timeout
store_forward_enabled = config['general'].getboolean('StoreForward', True)
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
reverseSF = config['general'].getboolean('reverseSF', False) # default False, send oldest first
welcome_message = config['general'].get('welcome_message', WELCOME_MSG)
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
motd_enabled = config['general'].getboolean('motdEnabled', True)
MOTD = config['general'].get('motd', MOTD)
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
whoami_enabled = config['general'].getboolean('whoami', True)
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
bee_enabled = config['general'].getboolean('bee', False) # 🐝 off by default undocumented
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
use_kiwix_server = config['general'].getboolean('useKiwixServer', False)
kiwix_url = config['general'].get('kiwixURL', 'http://127.0.0.1:8080')
kiwix_library_name = config['general'].get('kiwixLibraryName', 'wikipedia_en_100_nopic_2024-06')
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
llmModel = config['general'].get('ollamaModel', 'gemma3:270m') # default gemma3:270m
rawLLMQuery = config['general'].getboolean('rawLLMQuery', True) #default True
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True) # default True
llmUseWikiContext = config['general'].getboolean('llmUseWikiContext', False) # default False
useOpenWebUI = config['general'].getboolean('useOpenWebUI', False) # default False
openWebUIURL = config['general'].get('openWebUIURL', 'http://localhost:3000') # default localhost:3000
openWebUIAPIKey = config['general'].get('openWebUIAPIKey', '') # default empty
dont_retry_disconnect = config['general'].getboolean('dont_retry_disconnect', False) # default False, retry on disconnect
favoriteNodeList = config['general'].get('favoriteNodeList', '').split(',')
enableEcho = config['general'].getboolean('enableEcho', False) # default False
echoChannel = config['general'].getint('echoChannel', '9') # default 9, empty string to ignore
rssEnable = config['general'].getboolean('rssEnable', True) # default True
rssFeedURL = config['general'].get('rssFeedURL', 'http://www.hackaday.com/rss.xml,https://www.arrl.org/rss/arrl.rss').split(',')
rssMaxItems = config['general'].getint('rssMaxItems', 3) # default 3 items
rssTruncate = config['general'].getint('rssTruncate', 100) # default 100 characters
rssFeedNames = config['general'].get('rssFeedNames', 'default,arrl').split(',')
newsAPI_KEY = config['general'].get('newsAPI_KEY', '') # default empty
newsAPIregion = config['general'].get('newsAPIregion', 'us') # default us
enable_headlines = config['general'].getboolean('enableNewsAPI', False) # default False
newsAPIsort = config['general'].get('sort_by', 'relevancy') # default publishedAt
# sentry
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
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
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
highfly_interface = config['sentry'].getint('highFlyingAlertInterface', 1) # default 1
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
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
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
riverListDefault = config['location'].get('riverList', '').split(',') # default None
coastalEnabled = config['location'].getboolean('coastalEnabled', False) # default False
myCoastalZone = config['location'].get('myCoastalZone', None) # default None
coastalForecastDays = config['location'].getint('coastalForecastDays', 3) # default 3 days
# location alerts
eAlertBroadcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # old deprecated name
ipawsAlertEnabled = config['location'].getboolean('ipawsAlertEnabled', False) # default False new ^
# Keep both in sync for backward compatibility
if eAlertBroadcastEnabled or ipawsAlertEnabled:
eAlertBroadcastEnabled = True
ipawsAlertEnabled = True
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
myStateFIPSList = config['location'].get('myFIPSList', '').split(',') # default empty
mySAMEList = config['location'].get('mySAMEList', '').split(',') # default empty
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
eAlertBroadcastChannel = config['location'].getint('eAlertBroadcastChannel', '') # default empty
# any US alerts enabled
usAlerts = (
ipawsAlertEnabled or
wxAlertBroadcastEnabled or
volcanoAlertBroadcastEnabled or
wxAlertsEnabled or
eAlertBroadcastEnabled
)
# emergency response
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
emergency_responder_alert_interface = config['emergencyHandler'].getint('alert_interface', 1) # default 1
emergency_responder_email = config['emergencyHandler'].get('email', '').split(',')
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
bbsAPI_enabled = config['bbs'].getboolean('bbsAPI_enabled', False)
# checklist
checklist_enabled = config['checklist'].getboolean('enabled', False)
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
checklist_auto_approve = config['checklist'].getboolean('auto_approve', True) # default True
# qrz hello
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
train_qrz = config['qrz'].getboolean('training', True)
# inventory and POS
inventory_enabled = config['inventory'].getboolean('enabled', False)
inventory_db = config['inventory'].get('inventory_db', 'data/inventory.db')
disable_penny = config['inventory'].getboolean('disable_penny', False)
# E-Mail Settings
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
enableImap = config['smtp'].getboolean('enableImap', False)
SMTP_SERVER = config['smtp'].get('SMTP_SERVER', 'smtp.gmail.com')
SMTP_PORT = config['smtp'].getint('SMTP_PORT', 587)
FROM_EMAIL = config['smtp'].get('FROM_EMAIL', 'none@gmail.com')
SMTP_AUTH = config['smtp'].getboolean('SMTP_AUTH', True)
SMTP_USERNAME = config['smtp'].get('SMTP_USERNAME', FROM_EMAIL)
SMTP_PASSWORD = config['smtp'].get('SMTP_PASSWORD', 'password')
EMAIL_SUBJECT = config['smtp'].get('EMAIL_SUBJECT', 'Meshtastic✉')
IMAP_SERVER = config['smtp'].get('IMAP_SERVER', 'imap.gmail.com')
IMAP_PORT = config['smtp'].getint('IMAP_PORT', 993)
IMAP_USERNAME = config['smtp'].get('IMAP_USERNAME', SMTP_USERNAME)
IMAP_PASSWORD = config['smtp'].get('IMAP_PASSWORD', SMTP_PASSWORD)
IMAP_FOLDER = config['smtp'].get('IMAP_FOLDER', 'inbox')
# repeater
repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
# scheduler
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
schedulerInterval = config['scheduler'].get('interval', '') # default empty
schedulerTime = config['scheduler'].get('time', '') # default empty
schedulerValue = config['scheduler'].get('value', '') # default empty
schedulerMotd = config['scheduler'].getboolean('schedulerMotd', False) # default False
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
dxspotter_enabled = config['radioMon'].getboolean('dxspotter_enabled', True) # default True
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
sigWatchBroadcastInterface = config['radioMon'].getint('sigWatchBroadcastInterface', 1) # default interface 1
signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
voxDetectionEnabled = config['radioMon'].getboolean('voxDetectionEnabled', False) # default VOX detection disabled
voxDescription = config['radioMon'].get('voxDescription', 'VOX') # default VOX detected audio message
useLocalVoxModel = config['radioMon'].getboolean('useLocalVoxModel', False) # default False
localVoxModelPath = config['radioMon'].get('localVoxModelPath', 'no') # default models/vox.tflite
voxLanguage = config['radioMon'].get('voxLanguage', 'en-US') # default en-US
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
voxEnableCmd = config['radioMon'].getboolean('voxEnableCmd', True) # default True
meshagesTTS = config['radioMon'].getboolean('meshagesTTS', False) # default False
ttsChannels = config['radioMon'].get('ttsChannels', '2').split(',') # default Channel 2
ttsnoWelcome = config['radioMon'].getboolean('ttsnoWelcome', False) # default False
# WSJT-X and JS8Call monitoring
wsjtx_detection_enabled = config['radioMon'].getboolean('wsjtxDetectionEnabled', False) # default WSJT-X detection disabled
wsjtx_udp_server_address = config['radioMon'].get('wsjtxUdpServerAddress', '127.0.0.1:2237') # default localhost:2237
wsjtx_watched_callsigns = config['radioMon'].get('wsjtxWatchedCallsigns', '') # default empty (all callsigns)
js8call_detection_enabled = config['radioMon'].getboolean('js8callDetectionEnabled', False) # default JS8Call detection disabled
js8call_server_address = config['radioMon'].get('js8callServerAddress', '127.0.0.1:2442') # default localhost:2442
js8call_watched_callsigns = config['radioMon'].get('js8callWatchedCallsigns', '') # default empty (all callsigns)
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', '../data/news.txt') # default ../data/news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
allowXcmd = config['fileMon'].getboolean('allowXcmd', False) # default False
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', True) # default True
xCmd2factor_timeout = config['fileMon'].getint('2factor_timeout', 100) # default 100 seconds
# games
game_hop_limit = config['games'].getint('game_hop_limit', 5) # default 5 hops
disable_emojis_in_games = config['games'].getboolean('disable_emojis', False) # default False
dopewars_enabled = config['games'].getboolean('dopeWars', True)
lemonade_enabled = config['games'].getboolean('lemonade', True)
blackjack_enabled = config['games'].getboolean('blackjack', True)
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
mastermind_enabled = config['games'].getboolean('mastermind', True)
golfSim_enabled = config['games'].getboolean('golfSim', True)
hangman_enabled = config['games'].getboolean('hangman', True)
hamtest_enabled = config['games'].getboolean('hamtest', True)
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
quiz_enabled = config['games'].getboolean('quiz', False)
survey_enabled = config['games'].getboolean('survey', False)
default_survey = config['games'].get('defaultSurvey', 'example') # default example
surveyRecordID = config['games'].getboolean('surveyRecordID', True)
surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True)
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
except KeyError as e:
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160 chars
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 bytes
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
DEBUGpacket = config['messagingSettings'].getboolean('DEBUGpacket', False) # default False
noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False
logMetaStats = config['messagingSettings'].getboolean('logMetaStats', True) # default True
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
except Exception as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")
print("System: Check the config.ini against config.template file for missing sections or values.")
print("System: Exiting...")
exit(1)

277
modules/smtp.py Normal file
View File

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

View File

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

255
modules/space.py Normal file
View File

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

280
modules/survey.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

459
modules/test_bot.py Normal file
View File

@@ -0,0 +1,459 @@
# test_bot.py
# Unit tests for various modules in the meshing-around project
import os
import sys
# Add the parent directory to sys.path to allow module imports
parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, parent_path)
import unittest
import importlib
import pkgutil
import warnings
from modules.log import logger
from modules.settings import latitudeValue, longitudeValue
# Suppress ResourceWarning warnings for asyncio unclosed event here
warnings.filterwarnings("ignore", category=ResourceWarning)
modules_path = os.path.join(parent_path, 'modules')
# Limits API calls during testing
CHECKALL = False
# Check for a file named .checkall in the parent directory
checkall_path = os.path.join(parent_path, '.checkall')
if os.path.isfile(checkall_path):
CHECKALL = True
# List of module names to exclude
exclude = ['test_bot','udp', 'system', 'log', 'gpio', 'web',]
available_modules = [
m.name for m in pkgutil.iter_modules([modules_path])
if m.name not in exclude]
try:
print("\nImporting Core Modules:")
from modules.log import logger, getPrettyTime
print(" ✔ Imported 'log'")
# Set location default
lat = latitudeValue
lon = longitudeValue
print(f" ✔ Location set to Latitude: {lat}, Longitude: {lon}")
from modules.system import *
print(" ✔ Imported 'system'")
print("\nImporting non-excluded modules:")
for module_name in [m.name for m in pkgutil.iter_modules([modules_path])]:
if module_name not in exclude:
importlib.import_module(module_name)
print(f" ✔ Imported '{module_name}'")
except Exception as e:
print(f"\nError importing modules: {e}")
print("Run this program from the main program directory: python3 script/test_bot.py")
exit(1)
class TestBot(unittest.TestCase):
def test_example(self):
# Example test case
self.assertEqual(1 + 1, 2)
def test_load_bbsdb(self):
from bbstools import load_bbsdb
test_load = load_bbsdb()
self.assertTrue(test_load)
def test_bbs_list_messages(self):
from bbstools import bbs_list_messages
messages = bbs_list_messages()
print("list_messages() returned:", messages)
self.assertIsInstance(messages, str)
def test_initialize_checklist_database(self):
from checklist import initialize_checklist_database, process_checklist_command
result = initialize_checklist_database()
result1 = process_checklist_command(0, 'checklist', name="none", location="none")
self.assertTrue(result)
self.assertIsInstance(result1, str)
def test_init_news_sources(self):
from filemon import initNewsSources
result = initNewsSources()
self.assertTrue(result)
def test_get_nina_alerts(self):
from globalalert import get_nina_alerts
alerts = get_nina_alerts()
self.assertIsInstance(alerts, str)
def test_llmTool_get_google(self):
from llm import llmTool_get_google
result = llmTool_get_google("What is 2+2?", 1)
self.assertIsInstance(result, list)
def test_send_ollama_query(self):
from llm import send_ollama_query
response = send_ollama_query("Hello, Ollama!")
self.assertIsInstance(response, str)
def test_extract_search_terms(self):
from llm import extract_search_terms
# Test with capitalized terms
terms = extract_search_terms("What is Python programming?")
self.assertIsInstance(terms, list)
self.assertTrue(len(terms) > 0)
# Test with multiple capitalized words
terms2 = extract_search_terms("Tell me about Albert Einstein and Marie Curie")
self.assertIsInstance(terms2, list)
self.assertTrue(len(terms2) > 0)
def test_get_wiki_context(self):
from llm import get_wiki_context
# Test with a well-known topic
context = get_wiki_context("Python programming language")
self.assertIsInstance(context, str)
# Context might be empty if wiki is disabled or fails, that's ok
def test_get_moon_phase(self):
from space import get_moon
phase = get_moon(lat, lon)
self.assertIsInstance(phase, str)
def test_get_sun_times(self):
from space import get_sun
sun_times = get_sun(lat, lon)
self.assertIsInstance(sun_times, str)
def test_hf_band_conditions(self):
from space import hf_band_conditions
conditions = hf_band_conditions()
self.assertIsInstance(conditions, str)
def test_get_wikipedia_summary(self):
from wiki import get_wikipedia_summary
summary = get_wikipedia_summary("Python", location=(lat, lon))
self.assertIsInstance(summary, str)
def test_get_kiwix_summary(self):
from wiki import get_kiwix_summary
summary = get_kiwix_summary("Python")
self.assertIsInstance(summary, str)
def get_openskynetwork(self):
from locationdata import get_openskynetwork
flights = get_openskynetwork(lat, lon)
self.assertIsInstance(flights, str)
def test_initalize_qrz_database(self):
from qrz import initalize_qrz_database
result = initalize_qrz_database()
self.assertTrue(result)
def test_get_hamlib(self):
from radio import get_hamlib
frequency = get_hamlib('f')
self.assertIsInstance(frequency, str)
def test_get_rss_feed(self):
from rss import get_rss_feed
result = get_rss_feed('')
self.assertIsInstance(result, str)
##### GAMES Tests #####
def test_jokes(self):
from modules.games.joke import tell_joke
haha = tell_joke(nodeID=0, test=True)
print("Joke response:", haha)
self.assertIsInstance(haha, str)
def test_tictactoe_initial_and_move(self):
from games.tictactoe import tictactoe
user_id = "testuser"
# Start a new game (no move yet)
initial = tictactoe.play(user_id, "")
print("Initial response:", initial)
# Make a move, e.g., '1'
second = tictactoe.play(user_id, "1")
print("After move '1':", second)
self.assertIsInstance(initial, str)
self.assertIsInstance(second, str)
def test_playVideoPoker(self):
from games.videopoker import playVideoPoker
user_id = "testuser"
# Start a new game/session
initial = playVideoPoker(user_id, 'deal')
print("Initial response:", initial)
# Place a 5-coin bet
after_bet = playVideoPoker(user_id, '5')
print("After placing 5-coin bet:", after_bet)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_bet, str)
def test_play_blackjack(self):
from games.blackjack import playBlackJack
user_id = "testuser"
# Start a new game/session
initial = playBlackJack(user_id, 'deal')
print("Initial response:", initial)
# Place a 5-chip bet
after_bet = playBlackJack(user_id, '5')
print("After placing 5-chip bet:", after_bet)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_bet, str)
def test_hangman_initial_and_guess(self):
from games.hangman import hangman
user_id = "testuser"
# Start a new game (no guess yet)
initial = hangman.play(user_id, "")
print("Initial response:", initial)
# Guess a letter, e.g., 'e'
second = hangman.play(user_id, "e")
print("After guessing 'e':", second)
self.assertIsInstance(initial, str)
self.assertIsInstance(second, str)
def test_play_lemonade_stand(self):
from games.lemonade import playLemonstand, lemonadeTracker
user_id = "testuser"
# Ensure user is in tracker
if not any(u['nodeID'] == user_id for u in lemonadeTracker):
lemonadeTracker.append({'nodeID': user_id, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': 30.0, 'start': 30.0, 'cmd': 'new', 'last_played': 0})
# Start a new game
initial = playLemonstand(user_id, "", newgame=True)
print("Initial response:", initial)
# Buy 1 box of cups
after_cups = playLemonstand(user_id, "1")
print("After buying 1 box of cups:", after_cups)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_cups, str)
def test_play_golfsim_one_hole(self):
from games.golfsim import playGolf
user_id = "testuser"
# Start a new game/hole
initial = playGolf(user_id, "", last_cmd="new")
print("Initial hole info:", initial)
# Take first shot with driver
after_shot = playGolf(user_id, "driver")
print("After hitting driver:", after_shot)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_shot, str)
def test_play_dopewar_choose_city_and_list(self):
from games.dopewar import playDopeWars
user_id = 1234567899 # Use a unique test user ID
# Start a new game, get city selection prompt
initial = playDopeWars(user_id, "")
print("Initial city selection:", initial)
# Choose city 1
after_city = playDopeWars(user_id, "1")
print("After choosing city 1 (main game list):", after_city)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_city, str)
def test_play_mastermind_one_guess(self):
from games.mmind import start_mMind
user_id = 1234567899 # Use a unique test user ID
# Start a new game (should prompt for difficulty/colors)
initial = start_mMind(user_id, "n")
print("Initial response (difficulty/colors):", initial)
# Make a guess (e.g., "RGBY" - valid for normal)
after_guess = start_mMind(user_id, "RGBY")
print("After guessing RGBY:", after_guess)
self.assertIsInstance(initial, str)
self.assertIsInstance(after_guess, str)
def test_quiz_game_answer_one_and_end(self):
from games.quiz import quizGamePlayer
quizmaster_id = "admin" # Use a valid quizmaster ID from bbs_admin_list
user_id = "testuser"
# Start the quiz as quizmaster
start_msg = quizGamePlayer.start_game(quizmaster_id)
print("Quiz start:", start_msg)
# User joins the quiz
join_msg = quizGamePlayer.join(user_id)
print("User joined:", join_msg)
# Get the first question (should be included in join_msg, but call explicitly for clarity)
question_msg = quizGamePlayer.next_question(user_id)
print("First question:", question_msg)
# Simulate answering with 'A' (adjust if your first question expects a different answer)
answer_msg = quizGamePlayer.answer(user_id, "A")
print("Answer response:", answer_msg)
# End the quiz as quizmaster
end_msg = quizGamePlayer.stop_game(quizmaster_id)
print("Quiz end:", end_msg)
self.assertIsInstance(start_msg, str)
self.assertIsInstance(join_msg, str)
self.assertIsInstance(question_msg, str)
self.assertIsInstance(answer_msg, str)
self.assertIsInstance(end_msg, str)
def test_survey_answer_one_and_end(self):
from survey import survey_module
user_id = "testuser"
survey_name = "example" # Make sure this survey exists in your data/surveys directory
# Start the survey
start_msg = survey_module.start_survey(user_id, survey_name)
print("Survey start:", start_msg)
# Answer the first question with 'A' (adjust if your survey expects a different type)
answer_msg = survey_module.answer(user_id, "A")
print("Answer response:", answer_msg)
# End the survey
end_msg = survey_module.end_survey(user_id)
print("Survey end:", end_msg)
self.assertIsInstance(start_msg, str)
self.assertIsInstance(answer_msg, str)
self.assertIsInstance(end_msg, str)
def test_hamtest_answer_one(self):
from games.hamtest import hamtest
user_id = "testuser"
# Start a new ham test game (default level: technician)
initial = hamtest.newGame(user_id)
print("Initial question:", initial)
# Answer the first question with 'A'
answer_msg = hamtest.answer(user_id, "A")
print("Answer response:", answer_msg)
self.assertIsInstance(initial, str)
self.assertIsInstance(answer_msg, str)
##### API Tests - Extended tests run only if CHECKALL is True #####
if CHECKALL:
logger.info("Running extended API tests as CHECKALL is enabled.")
def test_handledxcluster(self):
from modules.dxspot import handledxcluster
test_message = "DX band=20m mode=SSB of=K7MHI"
response = handledxcluster(test_message, nodeID=0, deviceID='testdevice')
print("DX Spotter response:", response)
self.assertIsInstance(response, str)
def test_getRepeaterBook(self):
from locationdata import getRepeaterBook
repeaters = getRepeaterBook(lat, lon)
self.assertIsInstance(repeaters, str)
def test_getArtSciRepeaters(self):
from locationdata import getArtSciRepeaters
repeaters = getArtSciRepeaters(lat, lon)
self.assertIsInstance(repeaters, str)
def test_get_NOAAtides(self):
from locationdata import get_NOAAtide
tides = get_NOAAtide(lat, lon)
self.assertIsInstance(tides, str)
def test_get_NOAAweather(self):
from locationdata import get_NOAAweather
weather = get_NOAAweather(lat, lon)
self.assertIsInstance(weather, str)
def test_where_am_i(self):
from locationdata import where_am_i
location = where_am_i(lat, lon)
self.assertIsInstance(location, str)
def test_getWeatherAlertsNOAA(self):
from locationdata import getWeatherAlertsNOAA
alerts = getWeatherAlertsNOAA(lat, lon)
if isinstance(alerts, tuple):
self.assertIsInstance(alerts[0], str)
else:
self.assertIsInstance(alerts, str)
def test_getActiveWeatherAlertsDetailNOAA(self):
from locationdata import getActiveWeatherAlertsDetailNOAA
alerts_detail = getActiveWeatherAlertsDetailNOAA(lat, lon)
self.assertIsInstance(alerts_detail, str)
def test_getIpawsAlerts(self):
from locationdata import getIpawsAlert
alerts = getIpawsAlert(lat, lon)
self.assertIsInstance(alerts, str)
def test_get_flood_noaa(self):
from locationdata import get_flood_noaa
flood_info = get_flood_noaa(lat, lon, 12484500) # Example gauge UID
self.assertIsInstance(flood_info, str)
def test_get_volcano_usgs(self):
from locationdata import get_volcano_usgs
volcano_info = get_volcano_usgs(lat, lon)
self.assertIsInstance(volcano_info, str)
def test_get_nws_marine_alerts(self):
from locationdata import get_nws_marine
marine_alerts = get_nws_marine('https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt',1) # Example zone
self.assertIsInstance(marine_alerts, str)
def test_checkUSGSEarthQuakes(self):
from locationdata import checkUSGSEarthQuake
earthquakes = checkUSGSEarthQuake(lat, lon)
self.assertIsInstance(earthquakes, str)
def test_getNextSatellitePass(self):
from space import getNextSatellitePass
pass_info = getNextSatellitePass('25544', lat, lon)
self.assertIsInstance(pass_info, str)
def test_get_wx_meteo(self):
from wx_meteo import get_wx_meteo
weather_report = get_wx_meteo(lat, lon)
self.assertIsInstance(weather_report, str)
def test_get_flood_openmeteo(self):
from wx_meteo import get_flood_openmeteo
flood_report = get_flood_openmeteo(lat, lon)
self.assertIsInstance(flood_report, str)
def test_check_callsign_match(self):
# Test the callsign filtering function for WSJT-X/JS8Call
from radio import check_callsign_match
# Test with empty filter (should match all)
self.assertTrue(check_callsign_match("CQ K7MHI CN87", []))
# Test exact match
self.assertTrue(check_callsign_match("CQ K7MHI CN87", ["K7MHI"]))
# Test case insensitive match
self.assertTrue(check_callsign_match("CQ k7mhi CN87", ["K7MHI"]))
self.assertTrue(check_callsign_match("CQ K7MHI CN87", ["k7mhi"]))
# Test no match
self.assertFalse(check_callsign_match("CQ W1AW FN31", ["K7MHI"]))
# Test multiple callsigns
self.assertTrue(check_callsign_match("CQ W1AW FN31", ["K7MHI", "W1AW"]))
self.assertTrue(check_callsign_match("K7MHI DE W1AW", ["K7MHI", "W1AW"]))
# Test portable/mobile suffixes
self.assertTrue(check_callsign_match("CQ K7MHI/P CN87", ["K7MHI"]))
self.assertTrue(check_callsign_match("W1AW-7", ["W1AW"]))
# Test no false positives with partial matches
self.assertFalse(check_callsign_match("CQ K7MHIX CN87", ["K7MHI"]))
self.assertFalse(check_callsign_match("K7 TEST", ["K7MHI"]))
if __name__ == '__main__':
if not CHECKALL:
print("\nNote: Extended API tests are skipped. To enable them, create a file named '.checkall' in the parent directory.\n")
unittest.main()

78
modules/test_checklist.py Normal file
View File

@@ -0,0 +1,78 @@
# modules/test_checklist.py
import os
import sys
# Add the parent directory to sys.path to allow module imports
parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, parent_path)
import unittest
from unittest.mock import patch
from checklist import process_checklist_command, initialize_checklist_database
import time
class TestProcessChecklistCommand(unittest.TestCase):
def setUp(self):
# Always start with a fresh DB
initialize_checklist_database()
# Patch settings for consistent test behavior
patcher1 = patch('modules.checklist.reverse_in_out', False)
patcher2 = patch('modules.checklist.bbs_ban_list', [])
patcher3 = patch('modules.checklist.bbs_admin_list', ['999'])
self.mock_reverse = patcher1.start()
self.mock_ban = patcher2.start()
self.mock_admin = patcher3.start()
self.addCleanup(patcher1.stop)
self.addCleanup(patcher2.stop)
self.addCleanup(patcher3.stop)
def test_checkin_command(self):
result = process_checklist_command(1, "checkin test note", name="TESTUSER", location=["loc"])
self.assertIn("Checked✅In: TESTUSER", result)
def test_checkout_command(self):
# First checkin
process_checklist_command(1, "checkin test note", name="TESTUSER", location=["loc"])
# Then checkout
result = process_checklist_command(1, "checkout", name="TESTUSER", location=["loc"])
self.assertIn("Checked⌛Out: TESTUSER", result)
def test_checkin_with_interval(self):
result = process_checklist_command(1, "checkin 15 hiking", name="TESTUSER", location=["loc"])
self.assertIn("monitoring every 15min", result)
def test_checkout_all(self):
# Multiple checkins
process_checklist_command(1, "checkin note1", name="TESTUSER", location=["loc"])
process_checklist_command(1, "checkin note2", name="TESTUSER", location=["loc"])
result = process_checklist_command(1, "checkout all", name="TESTUSER", location=["loc"])
self.assertIn("Checked out", result)
self.assertIn("check-ins for TESTUSER", result)
def test_checklistapprove_nonadmin(self):
process_checklist_command(1, "checkin foo", name="FOO", location=["loc"])
result = process_checklist_command(2, "checklistapprove 1", name="NOTADMIN", location=["loc"])
self.assertNotIn("approved", result)
def test_checklistdeny_nonadmin(self):
process_checklist_command(1, "checkin foo", name="FOO", location=["loc"])
result = process_checklist_command(2, "checklistdeny 1", name="NOTADMIN", location=["loc"])
self.assertNotIn("denied", result)
def test_help_command(self):
result = process_checklist_command(1, "checklist ?", name="TESTUSER", location=["loc"])
self.assertIn("Command: checklist", result)
def test_checklist_listing(self):
process_checklist_command(1, "checkin foo", name="FOO", location=["loc"])
result = process_checklist_command(1, "checklist", name="FOO", location=["loc"])
self.assertIsInstance(result, str)
self.assertIn("checked-In", result)
def test_invalid_command(self):
result = process_checklist_command(1, "foobar", name="FOO", location=["loc"])
self.assertEqual(result, "Invalid command.")
if __name__ == "__main__":
unittest.main()

126
modules/udp.py Normal file
View File

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

62
modules/web.py Normal file
View File

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

135
modules/wiki.py Normal file
View File

@@ -0,0 +1,135 @@
# meshbot wiki module
from modules.log import logger
from modules.settings import (use_kiwix_server, kiwix_url, kiwix_library_name,
urlTimeoutSeconds, wiki_return_limit, ERROR_FETCHING_DATA, wikipedia_enabled)
#import wikipedia # pip install wikipedia
import requests
import bs4 as bs
from urllib.parse import quote
def tag_visible(element):
"""Filter visible text from HTML elements for Kiwix"""
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
return False
if isinstance(element, bs.element.Comment):
return False
return True
def text_from_html(body):
"""Extract main article text from HTML content"""
soup = bs.BeautifulSoup(body, 'html.parser')
# Try to find the main content div (works for both Kiwix and Wikipedia HTML)
main = soup.find('div', class_='mw-parser-output')
if not main:
# Fallback: just use the body if main content div not found
main = soup.body
if not main:
return ""
texts = main.find_all(string=True)
visible_texts = filter(tag_visible, texts)
return " ".join(t.strip() for t in visible_texts if t.strip())
def get_kiwix_summary(search_term, truncate=True):
"""Query local Kiwix server for Wikipedia article using only search results."""
if search_term is None or search_term.strip() == "":
return ERROR_FETCHING_DATA
try:
search_encoded = quote(search_term)
search_url = f"{kiwix_url}/search?content={kiwix_library_name}&pattern={search_encoded}"
response = requests.get(search_url, timeout=urlTimeoutSeconds)
if response.status_code == 200 and "No results were found" not in response.text:
soup = bs.BeautifulSoup(response.text, 'html.parser')
results = soup.select('div.results ul li')
logger.debug(f"Kiwix: Found {len(results)} results in search results for:{search_term}")
for li in results[:3]:
a = li.find('a', href=True)
if not a:
continue
article_url = f"{kiwix_url}{a['href']}"
article_response = requests.get(article_url, timeout=urlTimeoutSeconds)
if article_response.status_code == 200:
text = text_from_html(article_response.text)
# Remove navigation and search jump text
# text = text.split("Jump to navigation", 1)[-1]
# text = text.split("Jump to search", 1)[-1]
sentences = text.split('. ')
summary = '. '.join(sentences[:wiki_return_limit])
if summary and not summary.endswith('.'):
summary += '.'
if truncate:
return summary.strip()[:500]
else:
return summary.strip()
logger.debug(f"System: No Kiwix Results for:{search_term}")
if wikipedia_enabled:
logger.debug("Kiwix: Falling back to Wikipedia API.")
return get_wikipedia_summary(search_term, force=True)
return ERROR_FETCHING_DATA
except Exception as e:
logger.warning(f"System: Error with Kiwix for:{search_term} URL:{search_url} {e}")
return ERROR_FETCHING_DATA
def get_wikipedia_summary(search_term, location=None, force=False, truncate=True):
if use_kiwix_server and not force:
return get_kiwix_summary(search_term)
if not search_term or not search_term.strip():
return ERROR_FETCHING_DATA
api_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{requests.utils.quote(search_term)}"
headers = {
"User-Agent": "MeshBot/1.0 (https://github.com/kkeeton/meshing-around; contact: youremail@example.com)"
}
try:
response = requests.get(api_url, timeout=5, headers=headers)
if response.status_code == 404:
logger.warning(f"System: No Wikipedia Results for:{search_term}")
return ERROR_FETCHING_DATA
response.raise_for_status()
data = response.json()
logger.debug(f"Wikipedia API response for '{search_term}': {len(data)} keys")
if "extract" not in data or not data.get("extract"):
#logger.debug(f"System: Wikipedia API returned no extract for:{search_term} (data: {data})")
return ERROR_FETCHING_DATA
if data.get("type") == "disambiguation" or "may refer to:" in data.get("extract", ""):
#logger.warning(f"System: Disambiguation page for:{search_term} (data: {data})")
# Fetch and parse the HTML disambiguation page
html_url = f"https://en.wikipedia.org/wiki/{requests.utils.quote(search_term)}"
html_resp = requests.get(html_url, timeout=5, headers=headers)
if html_resp.status_code == 200:
soup = bs.BeautifulSoup(html_resp.text, 'html.parser')
items = soup.select('div.mw-parser-output ul li a[href^="/wiki/"]')
choices = []
for a in items:
title = a.get('title')
href = a.get('href')
# Filter out non-article links
if title and href and ':' not in href:
choices.append(f"{title} (https://en.wikipedia.org{href})")
if len(choices) >= 5:
break
if choices:
return f"'{search_term}' is ambiguous. Did you mean:\n- " + "\n- ".join(choices)
return f"'{search_term}' is ambiguous. Please be more specific. See: {html_url}"
summary = data.get("extract")
if not summary or not isinstance(summary, str) or not summary.strip():
#logger.debug(f"System: No summary found for:{search_term} (data: {data})")
return ERROR_FETCHING_DATA
sentences = [s for s in summary.split('. ') if s.strip()]
if not sentences:
return ERROR_FETCHING_DATA
summary = '. '.join(sentences[:wiki_return_limit])
if summary and not summary.endswith('.'):
summary += '.'
if truncate:
# Truncate to 500 characters
return summary.strip()[:500]
else:
return summary.strip()
except Exception as e:
logger.warning(f"System: Wikipedia API error for:{search_term} {e}")
return ERROR_FETCHING_DATA

View File

@@ -1,18 +1,20 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#import requests_cache
from modules.log import *
#import openmeteo_requests # pip install openmeteo-requests
#from retry_requests import retry # pip install retry_requests
import requests
import json
from modules.log import logger
from modules.settings import ERROR_FETCHING_DATA
def get_weather_data(api_url, params):
response = requests.get(api_url, params=params)
response.raise_for_status() # Raise an error for bad status codes
return response.json()
def get_wx_meteo(lat=0, lon=0, unit=0):
# set forcast days 1 or 3
forecastDays = 3
# Setup the Open-Meteo API client with cache and retry on error
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
retry_session = retry(retries = 3, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
@@ -34,27 +36,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
weather_data = get_weather_data(url, params)
except Exception as e:
logger.error(f"Error fetching meteo weather data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
response = responses[0]
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
# Process location
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
# Ensure response is defined
response = weather_data
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
daily = response['daily']
daily_weather_code = daily['weather_code']
daily_temperature_2m_max = daily['temperature_2m_max']
daily_temperature_2m_min = daily['temperature_2m_min']
daily_precipitation_hours = daily['precipitation_hours']
daily_precipitation_probability_max = daily['precipitation_probability_max']
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
@@ -94,10 +98,20 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
code_string = ""
if daily_weather_code[i] == 0:
code_string = "Clear sky"
elif daily_weather_code[i] == 1 or 2 or 3:
code_string = "Partly cloudy"
elif daily_weather_code[i] == 45 or 48:
elif daily_weather_code[i] == 1:
code_string = "Mostly Cloudy"
elif daily_weather_code[i] == 2:
code_string = "Partly Cloudy"
elif daily_weather_code[i] == 3:
code_string = "Overcast"
elif daily_weather_code[i] == 5:
code_string = "Haze"
elif daily_weather_code[i] == 10:
code_string = "Mist"
elif daily_weather_code[i] == 45:
code_string = "Fog"
elif daily_weather_code[i] == 48:
code_string = "Freezing Fog"
elif daily_weather_code[i] == 51:
code_string = "Drizzle: Light"
elif daily_weather_code[i] == 53:
@@ -126,6 +140,10 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
code_string = "Snow: Heavy"
elif daily_weather_code[i] == 77:
code_string = "Snow Grains"
elif daily_weather_code[i] == 78:
code_string = "Ice Crystals"
elif daily_weather_code[i] == 79:
code_string = "Ice Pellets"
elif daily_weather_code[i] == 80:
code_string = "Rain showers: Slight"
elif daily_weather_code[i] == 81:
@@ -133,15 +151,17 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
elif daily_weather_code[i] == 82:
code_string = "Rain showers: Heavy"
elif daily_weather_code[i] == 85:
code_string = "Snow showers: Light"
code_string = "Snow showers"
elif daily_weather_code[i] == 86:
code_string = "Snow showers: Moderate"
code_string = "Snow showers: Heavy"
elif daily_weather_code[i] == 95:
code_string = "Thunderstorm: Slight"
code_string = "Thunderstorm"
elif daily_weather_code[i] == 96:
code_string = "Thunderstorm: Moderate"
code_string = "Hailstorm"
elif daily_weather_code[i] == 97:
code_string = "Thunderstorm Heavy"
elif daily_weather_code[i] == 99:
code_string = "Thunderstorm: Heavy"
code_string = "Hailstorm Heavy"
weather_report += "Cond: " + code_string + ". "
@@ -175,3 +195,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
return weather_report
def get_flood_openmeteo(lat=0, lon=0):
# set forcast days 1 or 3
forecastDays = 3
# Flood data
url = "https://flood-api.open-meteo.com/v1/flood"
params = {
"latitude": {lat},
"longitude": {lon},
"timezone": "auto",
"daily": "river_discharge",
"forecast_days": forecastDays
}
try:
# Fetch the flood data
flood_data = get_weather_data(url, params)
except Exception as e:
logger.error(f"Error fetching meteo flood data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
# Ensure response is defined
response = flood_data
# Process daily data. The order of variables needs to be the same as requested.
daily = response['daily']
daily_river_discharge = daily['river_discharge']
# check if none
except Exception as e:
logger.error(f"Error processing meteo flood data: {e}")
return ERROR_FETCHING_DATA
# create a flood report
flood_report = ""
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
return flood_report

View File

@@ -2,30 +2,44 @@
# Meshtastic Autoresponder PONG Bot
# K7MHI Kelly Keeton 2024
try:
from pubsub import pub
except ImportError:
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh pong' using a virtual environment.")
exit(1)
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
from modules.log import *
from datetime import datetime
import random
from modules.log import logger, CustomFormatter, msgLogger
import modules.settings as my_settings
from modules.system import *
responseDelay = 0.7 # delay in seconds for response to avoid message collision
# Global Variables
DEBUGpacket = False # Debug print the packet rx
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
# Auto response to messages
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓Ping!!",
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"echo": lambda: handle_echo(message, message_from_id, deviceID, isDM, channel_number),
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"motd": lambda: handle_motd(message, MOTD),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(hop, snr, rssi),
"test": lambda: handle_testing(hop, snr, rssi),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
}
cmds = [] # list to hold the commands found in the message
for key in command_handler:
@@ -38,204 +52,387 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
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
def handle_ping(message, hop, snr, rssi):
if "@" in message:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
else:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🏓PONG, " + hop
def handle_cmd(message, message_from_id, deviceID):
# why CMD? its just a command list. a terminal would normally use "Help"
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
if " " in message and message.split(" ")[1] in trap_list:
return "🤖 just use the commands directly in chat"
return help_message
def handle_motd(message):
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
msg = ""
type = ''
if "ping" in message.lower():
msg = "🏓PONG"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
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(deviceID, 'short', 1)
elif deviceID == 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?"
# append SNR/RSSI or hop info
if hop.startswith("Gateway") or hop.startswith("MQTT"):
msg += " [GW]"
elif hop.startswith("Direct"):
msg += " [RF]"
else:
#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]
type = type + " @" + message.split("@")[1]
elif "#" in message:
msg = msg + " #" + message.split("#")[1]
type = type + " #" + message.split("#")[1]
# check for multi ping request
if " " in message:
# if stop multi ping
if "stop" in message.lower():
for i in range(0, len(multiPingList)):
if multiPingList[i].get('message_from_id') == message_from_id:
multiPingList.pop(i)
msg = "🛑 auto-ping"
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not my_settings.autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except ValueError:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not my_settings.useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
def handle_motd(message, message_from_id, isDM):
global MOTD
if "$" in message:
isAdmin = False
msg = MOTD
# check if the message_from_id is in the bbs_admin_list
if my_settings.bbs_admin_list != ['']:
for admin in my_settings.bbs_admin_list:
if str(message_from_id) == admin:
isAdmin = True
break
else:
isAdmin = True
# admin help via DM
if "?" in message and isDM and isAdmin:
msg = "Message of the day, set with 'motd $ HelloWorld!'"
elif "?" in message and isDM and not isAdmin:
# non-admin help via DM
msg = "Message of the day"
elif "$" in message and isAdmin:
motd = message.split("$")[1]
MOTD = motd.rstrip()
return "MOTD Set to: " + MOTD
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
msg = "MOTD changed to: " + MOTD
else:
return MOTD
msg = "MOTD: " + MOTD
return msg
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
if "?" in message.lower():
return "echo command returns your message back to you. Example:echo Hello World"
elif "echo " in message.lower():
parts = message.lower().split("echo ", 1)
if len(parts) > 1 and parts[1].strip() != "":
echo_msg = parts[1]
if channel_number != my_settings.echoChannel:
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
return echo_msg
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
else:
return "Please provide a message to echo back to you. Example:echo Hello World"
def sysinfo(message, message_from_id, deviceID):
if "?" in message:
return "sysinfo command returns system information."
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
# display last heard nodes add to response
bot_response = "Last Heard\n"
bot_response += str(get_node_list(1))
# bot_response += getNodeTelemetry(deviceID)
return bot_response
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "✋ACK-ACK! " + hop
def handle_testing(hop, snr, rssi):
if hop == "Direct":
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🎙Testing 1,2,3 " + hop
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxInterface}")
if port1 in rxInterface:
retry_int1 = True
elif interface2_enabled and port2 in rxInterface:
retry_int2 = True
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxHost}")
if hostname1 in rxHost and interface1_type == 'tcp':
retry_int1 = True
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
retry_int2 = True
if rxType == 'BLEInterface':
logger.critical(f"System: Lost Connection to Device BLE")
if interface1_type == 'ble':
retry_int1 = True
elif interface2_enabled and interface2_type == 'ble':
retry_int2 = True
def onReceive(packet, interface):
# extract interface defailts from interface object
global seenNodes
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
# extract interface details from inbound packet
rxType = type(interface).__name__
rxNode = 0
# Debug print the interface object
#for item in interface.__dict__.items(): print (item)
# Valies assinged to the packet
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
replyIDset = False
rxNodeHostName = None
emojiSeen = False
simulator_flag = False
isDM = False
channel_name = "unknown"
session_passkey = None
playingGame = False
if DEBUGpacket:
# Debug print the interface object
for item in interface.__dict__.items():
intDebug = f"{item}\n"
logger.debug(f"System: Packet Received on {rxType} Interface\n {intDebug} \n END of interface \n")
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# determine the rxNode based on the interface type
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
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 == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface:
rxNode = 1
elif interface2_enabled and port2 in rxInterface:
rxNode = 2
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'port{i}', '') in rxInterface),None)
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp':
rxNode = 1
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
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')
# 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
# Debug print the packet for debugging
#print(f"Packet Received\n {packet} \n END of packet \n")
message_from_id = 0
# 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 for a message packet and process it
snr = 0
rssi = 0
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
try:
snr = packet['rxSnr']
rssi = packet['rxRssi']
except KeyError:
snr = 0
rssi = 0
via_mqtt = packet['decoded'].get('viaMqtt', False)
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
# check if the packet is from us
if message_from_id 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'):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has replyId flag // currently unused in the code
if packet.get('replyId'):
replyIDset = packet.get('replyId', False)
# check if the packet has emoji flag set it // currently unused in the code
if packet.get('emoji'):
emojiSeen = packet.get('emoji', False)
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet['hopsAway']
else:
# if the packet does not have a hop count try other methods
hop_away = 0
if packet.get('hopLimit'):
hop_limit = packet['hopLimit']
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet['hopStart']
else:
hop_start = 0
hop_away = packet.get('hopsAway', 0)
if hop_start == hop_limit:
hop = "Direct"
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
hop_count = hop_away
else:
hop_count = hop_start - hop_limit
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
hop = f"{hop_count} hops"
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# calculate hop count
hop = ""
if hop_limit > 0 and hop_start >= hop_limit:
hop_count = hop_away + (hop_start - hop_limit)
elif hop_limit > 0 and hop_start < hop_limit:
hop_count = hop_away + (hop_limit - hop_start)
else:
hop_count = hop_away
if hop == "" 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 my_settings.enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} 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)}")
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
if messageTrap(message_string):
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
# log the message to stdout
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
# log the message to the message log
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
if ignoreDefaultChannel and channel_number == publicChannel:
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
else:
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
# or respond to channel message on the channel itself
if channel_number == my_settings.publicChannel and my_settings.antiSpam:
# warning user spamming default channel
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history list
if zuluTime:
if my_settings.zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
timestamp = datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
@@ -253,63 +450,236 @@ def onReceive(packet, interface):
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
if my_settings.repeater_enabled and multiple_interface:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
time.sleep(my_settings.responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
if str(channel_number) in my_settings.repeater_channels:
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
send_message(rMsg, channel_number, 0, i)
time.sleep(my_settings.responseDelay)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode, channel_number)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if sentry_enabled:
logger.debug(f"System: Sentry Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
logger.debug("System: RX Subscriber started")
# here we go loopty loo
while True:
await asyncio.sleep(0.5)
pass
def handle_boot(mesh=True):
try:
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
if mesh:
for i in range(1, 10):
if globals().get(f'interface{i}_enabled', False):
myNodeNum = globals().get(f'myNodeNum{i}', 0)
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {my_settings.llmModel} please wait")
llmLoad = llm_query(" ")
if "trouble" not in llmLoad:
logger.debug(f"System: LLM Model {my_settings.llmModel} loaded")
if my_settings.bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if my_settings.bbs_link_enabled:
if len(bbs_link_whitelist) > 0:
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
else:
logger.debug(f"System: BBS Link Enabled allowing all")
if my_settings.solar_conditions_enabled:
logger.debug("System: Celestial Telemetry Enabled")
if my_settings.location_enabled:
if my_settings.use_meteo_wxApi:
logger.debug("System: Location Telemetry Enabled using Open-Meteo API")
else:
logger.debug("System: Location Telemetry Enabled using NOAA API")
print("debug my_settings.scheduler_enabled:", my_settings.scheduler_enabled)
if my_settings.dad_jokes_enabled:
logger.debug("System: Dad Jokes Enabled!")
if my_settings.coastalEnabled:
logger.debug("System: Coastal Forecast and Tide Enabled!")
if games_enabled:
logger.debug("System: Games Enabled!")
if my_settings.wikipedia_enabled:
if my_settings.use_kiwix_server:
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_url}")
else:
logger.debug("System: Wikipedia search Enabled")
if my_settings.rssEnable:
logger.debug(f"System: RSS Feed Reader Enabled for feeds: {rssFeedNames}")
if my_settings.radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {my_settings.rigControlServerAddress} broadcasting to channels: {my_settings.sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if my_settings.file_monitor_enabled:
logger.warning(f"System: File Monitor Enabled for {my_settings.file_monitor_file_path}, broadcasting to channels: {my_settings.file_monitor_broadcastCh}")
if my_settings.enable_runShellCmd:
logger.debug("System: Shell Command monitor enabled")
if my_settings.allowXcmd:
logger.warning("System: File Monitor shell XCMD Enabled")
if my_settings.read_news_enabled:
logger.debug(f"System: File Monitor News Reader Enabled for {my_settings.news_file_path}")
if my_settings.bee_enabled:
logger.debug("System: File Monitor Bee Monitor Enabled for bee.txt")
if my_settings.wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {my_settings.wxAlertBroadcastChannel}")
if my_settings.emergencyAlertBrodcastEnabled:
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {my_settings.emergencyAlertBroadcastCh} for FIPS codes {my_settings.myStateFIPSList}")
if my_settings.myStateFIPSList == ['']:
logger.warning("System: No FIPS codes set for iPAWS Alerts")
if my_settings.emergency_responder_enabled:
logger.debug(f"System: Emergency Responder Enabled on channels {my_settings.emergency_responder_alert_channel} for interface {my_settings.emergency_responder_alert_interface}")
if my_settings.volcanoAlertBroadcastEnabled:
logger.debug(f"System: Volcano Alert Broadcast Enabled on channels {my_settings.volcanoAlertBroadcastChannel}")
if my_settings.qrz_hello_enabled:
if my_settings.train_qrz:
logger.debug("System: QRZ Welcome/Hello Enabled with training mode")
else:
logger.debug("System: QRZ Welcome/Hello Enabled")
if my_settings.enableSMTP:
if my_settings.enableImap:
logger.debug("System: SMTP Email Alerting Enabled using IMAP")
else:
logger.warning("System: SMTP Email Alerting Enabled")
# Default Options
if my_settings.useDMForResponse:
logger.debug("System: Respond by DM only")
if my_settings.log_messages_to_file:
logger.debug("System: Logging Messages to disk")
if my_settings.syslog_to_file:
logger.debug("System: Logging System Logs to disk")
if my_settings.motd_enabled:
logger.debug(f"System: MOTD Enabled using {my_settings.MOTD} scheduler:{my_settings.schedulerMotd}")
if my_settings.sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {my_settings.sentry_radius}m radius reporting to channel:{my_settings.secure_channel} requestLOC:{reqLocationEnabled}")
if my_settings.sentryIgnoreList:
logger.debug(f"System: Sentry BlockList Enabled for nodes: {my_settings.sentryIgnoreList}")
if my_settings.sentryWatchList:
logger.debug(f"System: Sentry WatchList Enabled for nodes: {my_settings.sentryWatchList}")
if my_settings.highfly_enabled:
logger.debug(f"System: HighFly Enabled using {my_settings.highfly_altitude}m limit reporting to channel:{my_settings.highfly_channel}")
if my_settings.store_forward_enabled:
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit} and reverse queue:{my_settings.reverseSF}")
if my_settings.enableEcho:
logger.debug("System: Echo command Enabled")
if my_settings.repeater_enabled and multiple_interface:
logger.debug(f"System: Repeater Enabled for Channels: {my_settings.repeater_channels}")
if my_settings.checklist_enabled:
logger.debug("System: CheckList Module Enabled")
if my_settings.ignoreChannels:
logger.debug(f"System: Ignoring Channels: {my_settings.ignoreChannels}")
if my_settings.noisyNodeLogging:
logger.debug("System: Noisy Node Logging Enabled")
if my_settings.logMetaStats:
logger.debug("System: Logging Metadata Stats Enabled, leaderboard")
if my_settings.scheduler_enabled:
logger.debug("System: Scheduler Enabled")
except Exception as e:
logger.error(f"System: Error during boot: {e}")
# Hello World
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
await asyncio.wait([meshRxTask, watchdogTask])
tasks = []
try:
handle_boot(mesh=False) # pong bot
# Create core tasks
tasks.append(asyncio.create_task(start_rx(), name="mesh_rx"))
tasks.append(asyncio.create_task(watchdog(), name="watchdog"))
# Add optional tasks
if my_settings.file_monitor_enabled:
tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor"))
if my_settings.radio_detection_enabled:
tasks.append(asyncio.create_task(handleSignalWatcher(), name="hamlib"))
try:
asyncLoop = asyncio.new_event_loop()
if __name__ == "__main__":
if my_settings.voxDetectionEnabled:
tasks.append(asyncio.create_task(voxMonitor(), name="vox_detection"))
if my_settings.scheduler_enabled:
from modules.scheduler import run_scheduler_loop, setup_scheduler
setup_scheduler(schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval)
tasks.append(asyncio.create_task(run_scheduler_loop(), name="scheduler"))
logger.debug(f"System: Starting {len(tasks)} async tasks")
# Wait for all tasks with proper exception handling
results = await asyncio.gather(*tasks, return_exceptions=True)
# Check for exceptions in results
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.error(f"Task {tasks[i].get_name()} failed with: {result}")
except Exception as e:
logger.error(f"Main loop error: {e}")
finally:
# Cleanup tasks
logger.debug("System: Cleaning up async tasks")
for task in tasks:
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
logger.debug(f"Task {task.get_name()} cancelled successfully")
except Exception as e:
logger.warning(f"Error cancelling task {task.get_name()}: {e}")
await asyncio.sleep(0.01)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
exit_handler()
pass
except KeyboardInterrupt:
exit_handler()
except SystemExit:
pass
# EOF

View File

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

102
script/README.md Normal file
View File

@@ -0,0 +1,102 @@
## 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.
## script/configMerge.py
**Purpose:**
`configMerge.py` is a Python script that merges your user configuration (`config.ini`) with the default template (`config.template`). This helps you keep your settings up to date when the default configuration changes, while preserving your customizations.
**Usage:**
Run this script from the project root or the `script/` directory:
```sh
python3 script/configMerge.py
```
**What it does:**
- Backs up your current `config.ini` to `config.bak`.
- Merges new or updated settings from `config.template` into your `config.ini`.
- Saves the merged result as `config_new.ini`.
- Shows a summary of changes between your config and the merged version.
**Note:**
After reviewing the changes, you can replace your `config.ini` with the merged version:
```sh
cp config_new.ini config.ini
```
This script is useful for safely updating your configuration when new options are added upstream.
## script/addFav.py
**Purpose:**
`addFav.py` is a Python script to help manage and add favorite nodes to all interfaces using data from `config.ini`. It supports both bot and roof (client_base) node workflows, making it easier to retain DM keys and manage node lists across devices.
**Usage:**
Run this script from the main repo directory:
```sh
python3 script/addFav.py
```
- To print the contents of `roofNodeList.pkl` and exit, use:
```sh
# note it is not production ready
python3 script/addFav.py -p
```
**What it does:**
- Interactively asks if you are running on a roof (client_base) node or a bot.
- On the bot:
- Compiles a list of favorite nodes and saves it to `roofNodeList.pkl` for later use on the roof node.
- On the roof node:
- Loads the node list from `roofNodeList.pkl`.
- Shows which favorite nodes will be added and asks for confirmation.
- Adds favorite nodes to the appropriate devices, handling API rate limits.
- Logs actions and errors for troubleshooting.
**Note:**
- Always run this script from the main repo directory to ensure module imports work.
- After running on the bot, copy `roofNodeList.pkl` to the roof node and rerun the script there to complete the process.

132
script/addFav.py Normal file
View File

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

104
script/configMerge.py Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More