Compare commits

...

405 Commits
v1.4 ... v1.5.0

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

6
.gitignore vendored
View File

@@ -7,8 +7,14 @@ config.ini
# virtualenv
venv/
# logs
logs/*.log
# modified .service files
etc/*.service
# Python cache
__pycache__/
# rag data
data/rag/*

470
README.md
View File

@@ -1,93 +1,114 @@
# 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")
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
## 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.
![Example Use](etc/pong-bot.jpg "Example Use")
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.
## Key Features
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`.
### Intelligent Keyword Responder
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
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`
### Network Tools
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location. For example having the bot in your camp site alerts when members arive back at camp.
### Dual Radio/Node Support
- **Simultaneous Monitoring**: Monitor two networks at the same time.
- **Flexible Messaging**: send mail and messages, between networks.
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.
### Advanced Messaging Capabilities
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach
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
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **Wiki Integration**: Look up data using Wikipedia results.
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
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
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
### Fun and Games
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
## Full list of commands for the bot
### Radio Frequency Monitoring
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
- 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`
- `bbsinfo` Stats on BBS delivery and messages (sysop)
- 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`
- `history` returns the last commands ran by user(s)
- `cmd` returns the list of commands (the help message)
- Games - via DM
- `lemonstand` plays the classic Lemonade Stand Finance
- `dopewars` plays the classic drug trader
- `blackjack` BlackJack, Casino 21
- `videopoker` Video Poker, basic 5 card hold
- `mastermind` Classic code-breaking game
- `golfsim` Golf Simulator, 9 Hole
### NOAA EAS Alerts
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
## pong_bot.sh
Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
### File Monitor Alerts
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
- **News File**: on request of news the contents of the file is returned.
## 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.
### Data Reporting
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
## 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`
### Robust Message Handling
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
Optionally:
- `install.sh` will automate optional venv and requirements installation.
- `launch.sh` will activate and launch the app in the venv if built.
## 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 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`
### 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
### Installation
#### Clone the Repository
```sh
git clone https://github.com/spudgunman/meshing-around
```
#config.ini
The code is under active development, so make sure to pull the latest changes regularly!
#### Optional Automation of setup
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
#### Docker Installation
If you prefer to use Docker, follow these steps:
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
2. Build the Docker image:
```sh
cd meshing-around
docker build -t meshing-around .
```
3. Run the Docker container:
```sh
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
```
#### Custom Install
Install the required dependencies using pip:
```sh
pip install -r requirements.txt
```
Copy the configuration template to `config.ini` and edit it to suit your needs:
```sh
cp config.template config.ini
```
### Configuration
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
```sh
meshtastic --ble-scan
```
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
```ini
# config.ini
# type can be serial, tcp, or ble.
# port is the serial port to use; commented out will try to auto-detect
# hostname is the IP address of the device to connect to for TCP type
# mac is the MAC address of the device to connect to for ble type
# mac is the MAC address of the device to connect to for BLE type
[interface]
type = serial
@@ -95,20 +116,24 @@ type = serial
# hostname = 192.168.0.1
# mac = 00:11:22:33:44:55
# Additional interface for dual radio support See config.template for more.
# 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.
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.
```
### General Settings
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
```ini
[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 Settings
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
```ini
[location]
enabled = True
lat = 48.50
@@ -116,8 +141,10 @@ lon = -123.0
UseMeteoWxAPI = True
```
Modules can be disabled or enabled.
```
### Module Settings
Modules can be enabled or disabled as needed.
```ini
[bbs]
enabled = False
@@ -125,97 +152,156 @@ enabled = False
DadJokes = False
StoreForward = False
```
History command is like a linix terminal, shows the last commands the user ran and the `lheard` reflects last users on the bot.
```
# history command
enableCmdHistory = True
# command history ignore list ex: 2813308004,4258675309
lheardCmdIgnoreNodes =
```
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!!!
### History
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
```ini
enableCmdHistory = True # history command enabler
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
```
# repeater module
[repeater]
### Sentry Settings
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
```ini
SentryEnabled = True # detect anyone close to the bot
SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
```
### Repeater Settings
A repeater function for two different nodes and cross-posting messages. The [`repeater_channels`] is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
```ini
[repeater] # repeater module
enabled = True
repeater_channels = [2, 3]
```
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.**
### Ollama (LLM/AI) Settings
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
```ini
# Enable ollama LLM see more at https://ollama.com
ollama = True # Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma2 #ollamaModel = llama3.1
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
```
Also see `llm.py` for changing the defaults of:
```ini
# LLM System Variables
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
### Radio Monitoring
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
```ini
[radioMon]
enabled = False
rigControlServerAddress = localhost:4532
# 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
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
signalHoldTime = 10 # hold time for high SNR
signalCooldown = 5 # the following are combined to reset the monitor
signalCycleLimit = 5
```
Ollama 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.
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
filemon_enabled = True
file_path = alert.txt
broadcastCh = 2,4
enable_read_news = False
news_file_path = news.txt
```
# Enable ollama LLM see more at https://ollama.com
ollama = True
# Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma2
#ollamaModel = llama3.1
#### NOAA EAS
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
```ini
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2,4
```
also see llm.py for changing the defaults of
To Monitor EAS with no internet connection see the following notes
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
no examples yet for these tools
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
- [dsame3](https://github.com/jamieden/dsame3)
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
```bash
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
```
# 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
The following example shell command will pipe rtl_sdr to alert.txt
```bash
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
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.
#### Newspaper on mesh
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
### Scheduler
In the config.ini enable the module
```ini
[scheduler]
# enable or disable the scheduler module
enabled = True
```
# 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))
The actions are via code only at this time. See mesh_bot.py around line [1050](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1050) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
# 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))
```python
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
#Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
```
# 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
#### BBS Link
The scheduler also handles the BBL Link Brodcast message, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
```
```ini
bbslink_enabled = True
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
bbslink_whitelist =
```
### MQTT Notes
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
### Requirements
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
```sh
pip install meshtastic
pip install pubsub
```
mesh-bot enhancements
```
Mesh-bot enhancements:
```sh
pip install pyephem
pip install requests
pip install geopy
@@ -226,36 +312,110 @@ pip install geopy
pip install schedule
pip install wikipedia
```
The following is needed for open-meteo use
```
For open-meteo use:
```sh
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
For the Ollama LLM:
```sh
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`
To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
## Full list of commands for the bot
### Networking
| Command | Description | ✅ Works Off-Grid |
|---------|-------------|-
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
| `test` | Returns like ping but also can be used to test the limits of data buffers `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
| `whereami` | Returns the address of the sender's location if known |
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
### Radio Propagation & Weather Forcasting
| Command | Description | |
|---------|-------------|-------------------
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `solar` | Gives an idea of the x-ray flux | |
| `hfcond` | Returns a table of HF solar conditions | |
| `tide` | Returns the local tides (NOAA data source) |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details |
### Bulletin Board & Mail
| Command | Description | |
|---------|-------------|-
| `bbshelp` | Returns the following help message | ✅ |
| `bbslist` | Lists the messages by ID and subject | ✅ |
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
### Data Lookup
| Command | Description | |
|---------|-------------|-
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
### Games (via DM)
| Command | Description | |
|---------|-------------|-
| `joke` | Tells a joke | ✅ |
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
# 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
- https://github.com/pwdkramer/pythonMastermind/
- https://github.com/danfriedman30/pythongame (Golf)
### 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, bitflip, and github Hailo1999, for testing and feature 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)
### Special Thanks
- **xdep**: For the reporting tools.
- **Nestpebble**: For new ideas and enhancements.
- **mrpatrick1991**: For Docker configurations.
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)

View File

@@ -25,7 +25,7 @@ port = /dev/ttyUSB0
[general]
# if False will respond on all channels but the default channel
respond_by_dm_only = True
# defaultChannel is the meshtastic default public channel, e.g. LongFast
# 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
@@ -36,6 +36,7 @@ welcome_message = MeshBot, here for you like a friend who is not. Try sending: p
whoami = True
# enable or disable the Joke module
DadJokes = True
DadJokesEmoji = False
# enable or disable the Solar module
spaceWeather = True
# enable or disable the wikipedia search module
@@ -44,6 +45,8 @@ wikipedia = True
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
@@ -58,9 +61,13 @@ urlTimeout = 10
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = False
SyslogToFile = True
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
[games]
# if hop limit for the user exceeds this value, the message will be dropped
game_hop_limit = 5
# enable or disable the games module(s)
dopeWars = True
lemonade = True
@@ -87,6 +94,10 @@ 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 =
# location module
[location]
@@ -101,6 +112,12 @@ NOAAalertCount = 2
UseMeteoWxAPI = False
# Default to metric units rather than imperial
useMetric = False
# repeaterList lookup location (rbook / artsci)
repeaterLookup = rbook
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# repeater module
[repeater]
@@ -109,7 +126,11 @@ enabled = False
# and rebroadcasted on the same channel on the other device/node/interface
# with great power comes great responsibility, danger could be lurking in use of this feature
# if you have the two nodes on the same radio configurations, you could create a feedback loop
repeater_channels =
repeater_channels =
[scheduler]
# enable or disable the scheduler module
enabled = False
[radioMon]
# using Hamlib rig control will monitor and alert on channel use
@@ -125,6 +146,13 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
[fileMon]
filemon_enabled = False
file_path = alert.txt
broadcastCh = 2
enable_read_news = False
news_file_path = news.txt
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7
@@ -132,3 +160,9 @@ responseDelay = 0.7
splitDelay = 0.0
# message chunk size for sending at high success rate
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max lilmit Buffer for radio testing
maxBuffer = 220

1
data/README.md Normal file
View File

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

View File

@@ -4,65 +4,65 @@ import pickle # pip install pickle
# load the bbs messages from the database file
try:
with open('../bbsdb.pkl', 'rb') as f:
with open('../data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
try:
with open('bbsdb.pkl', 'rb') as f:
with open('data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
bbs_messages = "System: bbsdb.pkl not found"
bbs_messages = "System: data/bbsdb.pkl not found"
try:
with open('../bbsdm.pkl', 'rb') as f:
with open('../data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except Exception as e:
try:
with open('bbsdm.pkl', 'rb') as f:
with open('data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except Exception as e:
bbs_dm = "System: bbsdm.pkl not found"
bbs_dm = "System: data/bbsdm.pkl not found"
# Game HS tables
try:
with open('../lemonade_hs.pkl', 'rb') as f:
with open('../data/lemonstand.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except Exception as e:
try:
with open('lemonade_hs.pkl', 'rb') as f:
with open('data/lemonstand.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except Exception as e:
lemon_score = "System: lemonade_hs.pkl not found"
lemon_score = "System: data/lemonstand.pkl not found"
try:
with open('../dopewar_hs.pkl', 'rb') as f:
with open('../data/dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except Exception as e:
try:
with open('dopewar_hs.pkl', 'rb') as f:
with open('data/dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except Exception as e:
dopewar_score = "System: dopewar_hs.pkl not found"
dopewar_score = "System: data/dopewar_hs.pkl not found"
try:
with open('../blackjack_hs.pkl', 'rb') as f:
with open('../data/blackjack_hs.pkl', 'rb') as f:
blackjack_score = pickle.load(f)
except Exception as e:
try:
with open('blackjack_hs.pkl', 'rb') as f:
with open('data/blackjack_hs.pkl', 'rb') as f:
blackjack_score = pickle.load(f)
except Exception as e:
blackjack_score = "System: blackjack_hs.pkl not found"
blackjack_score = "System: data/blackjack_hs.pkl not found"
try:
with open('../videopoker_hs.pkl', 'rb') as f:
with open('../data/videopoker_hs.pkl', 'rb') as f:
videopoker_score = pickle.load(f)
except Exception as e:
try:
with open('videopoker_hs.pkl', 'rb') as f:
with open('data/videopoker_hs.pkl', 'rb') as f:
videopoker_score = pickle.load(f)
except Exception as e:
videopoker_score = "System: videopoker_hs.pkl not found"
videopoker_score = "System: data/videopoker_hs.pkl not found"
try:
with open('../mmind_hs.pkl', 'rb') as f:
@@ -75,14 +75,14 @@ except Exception as e:
mmind_score = "System: mmind_hs.pkl not found"
try:
with open('../golfsim_hs.pkl', 'rb') as f:
with open('../data/golfsim_hs.pkl', 'rb') as f:
golfsim_score = pickle.load(f)
except Exception as e:
try:
with open('golfsim_hs.pkl', 'rb') as f:
with open('data/golfsim_hs.pkl', 'rb') as f:
golfsim_score = pickle.load(f)
except Exception as e:
golfsim_score = "System: golfsim_hs.pkl not found"
golfsim_score = "System: data/golfsim_hs.pkl not found"
print ("\n Meshing-Around Database Admin Tool\n")

48
etc/eas_alert_parser.py Normal file
View File

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

View File

@@ -0,0 +1,10 @@
[Unit]
Description=MeshingAround-ReportingTask
[Timer]
OnUnitActiveSec=1h
OnbootSec=5min
Unit=mesh_bot_reporting.service
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,25 @@
# /etc/systemd/system/mesh_bot.service
# sudo systemctl daemon-reload
# sudo systemctl start mesh_bot.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
# 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

990
etc/report_generator.py Normal file
View File

@@ -0,0 +1,990 @@
# -*- coding: utf-8 -*-
import os
import re
import sys
import glob
import json
import pickle
import platform
import requests
import subprocess
import configparser
from string import Template
from datetime import datetime
from importlib.metadata import version
from collections import Counter, defaultdict
# global variables
LOG_PATH = '/opt/meshing-around/logs' # override path to log files (defaults to ../log)
W3_PATH = '/var/www/html/' # override path to web server root (defaults to ../www)
multiLogReader = False # set to True to read all logs in ../log
shameWordList = ['password', 'combo', 'key', 'hidden', 'secret', 'pass', 'token', 'login', 'username', 'admin', 'root', 'base64:', '==' ]
# system variables
script_dir = os.path.dirname(os.path.realpath(__file__))
www_dir = os.path.join(script_dir, 'www')
config_file = os.path.join(script_dir, 'web_reporter.cfg')
# set up report.cfg as ini file
config = configparser.ConfigParser()
try:
config.read(config_file)
except Exception as e:
print(f"Error reading web_reporter.cfg: {str(e)} generating default config")
if config.sections() == []:
print(f"web_reporter.cfg is empty or does not exist, generating default config")
shameWordList = shameWordList_str = ', '.join(shameWordList)
config['reporting'] = {'log_path': script_dir, 'w3_path': www_dir, 'multi_log_reader': 'False', 'shame_word_list': shameWordList}
with open(config_file, 'w') as configfile:
config.write(configfile)
# read config file
LOG_PATH = config['reporting'].get('log_path', LOG_PATH)
W3_PATH = config['reporting'].get('w3_path', W3_PATH)
multiLogReader = config['reporting'].getboolean('multi_log_reader', multiLogReader)
# config['reporting']['shame_word_list'] is a comma-separated string
shameWordList = config['reporting'].get('shame_word_list', '')
if isinstance(shameWordList, str):
shameWordList = shameWordList.split(', ')
def parse_log_file(file_path):
global log_data
lines = ['']
# see if many logs are present
if multiLogReader:
# set file_path to the cwd of the default project ../log
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
print(f"Checking log files: {log_files}")
if log_files:
log_files.sort()
for logFile in log_files:
with open(os.path.join(log_dir, logFile), 'r') as file:
lines += file.readlines()
else:
try:
print(f"Checking log file: {file_path}")
with open(file_path, 'r') as file:
lines = file.readlines()
except FileNotFoundError:
print(f"Error: File not found at {file_path}")
sys.exit(1)
if multiLogReader:
print(f"Consumed {len(lines)} lines from {len(log_files)} log files")
else:
print(f"Consumed {len(lines)} lines from {file_path}")
log_data = {
'command_counts': Counter(),
'message_types': Counter(),
'llm_queries': Counter(),
'unique_users': set(),
'warnings': [],
'errors': [],
'hourly_activity': defaultdict(int),
'bbs_messages': 0,
'messages_waiting': 0,
'total_messages': 0,
'gps_coordinates': defaultdict(list),
'command_timestamps': [],
'message_timestamps': [],
'firmware1_version': "N/A",
'firmware2_version': "N/A",
'node1_uptime': "N/A",
'node2_uptime': "N/A",
'node1_name': "N/A",
'node2_name': "N/A",
'node1_ID': "N/A",
'node2_ID': "N/A",
'shameList': []
}
for line in lines:
timestamp_match = re.match(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+', line)
if timestamp_match:
timestamp = datetime.strptime(timestamp_match.group(1), '%Y-%m-%d %H:%M:%S')
log_data['hourly_activity'][timestamp.strftime('%Y-%m-%d %H:00:00')] += 1
if 'Bot detected Commands' in line or 'LLM Query:' in line or 'PlayingGame' in line:
# get the command and user from the line
command = re.search(r"'cmd': '(\w+)'", line)
user = re.search(r"From: (.+)$", line)
if 'LLM Query:' in line:
log_data['command_counts']['LLM Query'] += 1
log_data['command_timestamps'].append((timestamp.isoformat(), 'LLM Query'))
if 'PlayingGame' in line:
#log line looks like this. 2024-10-04 20:24:53,381 | DEBUG | System: 862418040 PlayingGame BlackJack last_cmd: new
game = re.search(r'PlayingGame (\w+)', line)
user = re.search(r'System: (\d+)', line)
log_data['command_counts'][game.group(1)] += 1
log_data['command_timestamps'].append((timestamp.isoformat(), game))
if 'Sending DM:' in line or 'Sending Multi-Chunk DM:' in line or 'SendingChannel:' in line or 'Sending Multi-Chunk Message:' in line:
log_data['message_types']['Outgoing DM'] += 1
log_data['total_messages'] += 1
log_data['message_timestamps'].append((timestamp.isoformat(), 'Outgoing DM'))
if 'Received DM:' in line or 'Ignoring DM:' in line or 'Ignoring Message:' in line or 'ReceivedChannel:' in line or 'LLM Query:' in line:
log_data['message_types']['Incoming DM'] += 1
log_data['total_messages'] += 1
# include a little of the message
if 'Ignoring Message:' in line:
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("Ignoring Message:")[1][:90]}'))
elif 'Ignoring DM:' in line:
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("Ignoring DM:")[1][:90]}'))
elif 'LLM Query:' in line:
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("LLM Query:")[1][:90]}'))
else:
log_data['message_timestamps'].append((timestamp.isoformat(), 'Incoming:'))
# check for shame words in the message
for word in shameWordList:
if word in line.lower():
if line not in log_data['shameList']:
line = line.replace('Ignoring Message:', '')
line = line.replace('|', '')
line = line.replace('INFO', '')
line = line.replace('DEBUG', '')
log_data['shameList'].insert(0, line)
# get the user who sent the message
if 'To: ' in line:
user_match = re.search(r"From: '([^']+)'(?: To:|$)", line)
else:
user_match = re.search(r"From: (.+)$", line)
if user_match:
log_data['unique_users'].add(user_match.group(1))
# Error Logs
if 'WARNING |' in line:
# remove some junk from the line
line = line.replace('|', '')
line = line.replace(' ', ' ')
log_data['warnings'].insert(0, line)
if 'ERROR |' in line or 'CRITICAL |' in line:
# remove some junk from the line
line = line.replace('System:', '')
line = line.replace('|', '')
line = line.replace(' ', ' ')
log_data['errors'].insert(0, line)
# bbs messages
bbs_match = re.search(r'📡BBSdb has (\d+) messages.*?Messages waiting: (\d+)', line)
if bbs_match:
bbs_messages = int(bbs_match.group(1))
messages_waiting = int(bbs_match.group(2))
log_data['bbs_messages'] = bbs_messages
log_data['messages_waiting'] = messages_waiting
gps_match = re.search(r'location data for (\d+) is ([-\d.]+),([-\d.]+)', line)
if gps_match:
node_id = None
node_id, lat, lon = gps_match.groups()
log_data['gps_coordinates'][node_id].append((float(lat), float(lon)))
# get telemetry data
if '| Telemetry:' in line:
telemetry_match = re.search(r'Telemetry:(\d+) numPacketsRx:(\d+) numPacketsRxErr:(\d+) numPacketsTx:(\d+) numPacketsTxErr:(\d+) ChUtil%:(\d+\.\d+) AirTx%:(\d+\.\d+) totalNodes:(\d+) Online:(\d+) Uptime:(\d+d) Volt:(\d+\.\d+) Firmware:(\d+\.\d+\.\d+\.\w+)', line)
if telemetry_match:
interface_number, numPacketsRx, numPacketsRxErr, numPacketsTx, numPacketsTxErr, ChUtil, AirTx, totalNodes, online, uptime, volt, firmware_version = telemetry_match.groups()
data = f"Tx: {numPacketsTx} Rx: {numPacketsRx} Uptime: {uptime} Volt: {volt} numPacketsRxErr: {numPacketsRxErr} numPacketsTxErr: {numPacketsTxErr} ChUtil: {ChUtil} AirTx: {AirTx} totalNodes: {totalNodes} Online: {online}"
if interface_number == '1':
log_data['firmware1_version'] = firmware_version
log_data['node1_uptime'] = data
log_data['nodeCount1'] = totalNodes
log_data['nodeCountOnline1'] = online
log_data['tx1'] = numPacketsTx
log_data['rx1'] = numPacketsRx
elif interface_number == '2':
log_data['firmware2_version'] = firmware_version
log_data['node2_uptime'] = data
log_data['nodeCount2'] = totalNodes
log_data['nodeCountOnline2'] = online
log_data['tx2'] = numPacketsTx
log_data['rx2'] = numPacketsRx
# get name and nodeID for devices
if 'Autoresponder Started for Device' in line:
device_match = re.search(r'Autoresponder Started for Device(\d+)\s+([^\s,]+).*?NodeID: (\d+)', line)
if device_match:
device_id = device_match.group(1)
device_name = device_match.group(2)
node_id = device_match.group(3)
if device_id == '1':
log_data['node1_name'] = device_name
log_data['node1_ID'] = node_id
elif device_id == '2':
log_data['node2_name'] = device_name
log_data['node2_ID'] = node_id
log_data['unique_users'] = list(log_data['unique_users'])
log_data['unique_users'].reverse()
return log_data
def get_system_info():
def get_command_output(command):
try:
return subprocess.check_output(command, shell=True).decode('utf-8').strip()
except subprocess.CalledProcessError:
return "N/A"
# Capture some system information from log_data
firmware1_version = log_data['firmware1_version']
firmware2_version = log_data['firmware2_version']
node1_uptime = log_data['node1_uptime']
node2_uptime = log_data['node2_uptime']
node1_name = log_data['node1_name']
node2_name = log_data['node2_name']
node1_ID = log_data['node1_ID']
node2_ID = log_data['node2_ID']
print(f"Node1: {node1_name} {node1_ID} {firmware1_version}")
print(f"Node2: {node2_name} {node2_ID} {firmware2_version}")
# get Meshtastic CLI version on web
try:
url = "https://pypi.org/pypi/meshtastic/json"
data = requests.get(url, timeout=5).json()
pypi_version = data["info"]["version"]
cli_web = f"v{pypi_version}"
except Exception:
pass
# get Meshtastic CLI version on local
try:
if "importlib.metadata" in sys.modules:
cli_local = version("meshtastic")
except:
pass # Python 3.7 and below, meh..
if platform.system() == "Linux":
uptime = get_command_output("uptime -p")
memory_total = get_command_output("free -m | awk '/Mem:/ {print $2}'")
memory_available = get_command_output("free -m | awk '/Mem:/ {print $7}'")
disk_total = get_command_output("df -h / | awk 'NR==2 {print $2}'")
disk_free = get_command_output("df -h / | awk 'NR==2 {print $4}'")
elif platform.system() == "Darwin": # macOS
uptime = get_command_output("uptime | awk '{print $3,$4,$5}'")
memory_total = get_command_output("sysctl -n hw.memsize | awk '{print $0/1024/1024}'")
memory_available = "N/A" # Not easily available on macOS without additional tools
disk_total = get_command_output("df -h / | awk 'NR==2 {print $2}'")
disk_free = get_command_output("df -h / | awk 'NR==2 {print $4}'")
else:
return {
'uptime': "N/A",
'memory_total': "N/A",
'memory_available': "N/A",
'disk_total': "N/A",
'disk_free': "N/A",
'interface1_version': "N/A",
'interface2_version': "N/A",
'node1_uptime': "N/A",
'node2_uptime': "N/A",
'node1_name': "N/A",
'node2_name': "N/A",
'node1_ID': "N/A",
'node2_ID': "N/A",
'cli_web': "N/A",
'cli_local': "N/A"
}
return {
'uptime': uptime,
'memory_total': f"{memory_total} MB",
'memory_available': f"{memory_available} MB" if memory_available != "N/A" else "N/A",
'disk_total': disk_total,
'disk_free': disk_free,
'interface1_version': firmware1_version,
'interface2_version': firmware2_version,
'node1_uptime': node1_uptime,
'node2_uptime': node2_uptime,
'node1_name': node1_name,
'node2_name': node2_name,
'node1_ID': node1_ID,
'node2_ID': node2_ID,
'cli_web': cli_web,
'cli_local': cli_local
}
def get_wall_of_shame():
# Get the wall of shame out of the log data
logShameList = log_data['shameList']
# future space for other ideas
return {
'shame': ', '.join(shameWordList),
'shameList': '\n'.join(f'<li>{line}</li>' for line in logShameList),
}
def get_database_info():
# ../config.ini location to script path
config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'config.ini')
# get config.ini variables
config = configparser.ConfigParser()
config.read(config_path)
# for section in config.sections():
# print(f"Section: {section}")
# for key in config[section]:
# print(f"Key: {key}, Value: {config[section][key]}")
banList = config['bbs'].get('bbs_ban_list', 'none')
adminList = config['bbs'].get('bbs_admin_list', 'none')
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', 'none')
# Define the base directory
base_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'data'))
# data files
databaseFiles = [os.path.join(base_dir, 'lemonstand_hs.pkl'),
os.path.join(base_dir, 'dopewar_hs.pkl'),
os.path.join(base_dir, 'blackjack_hs.pkl'),
os.path.join(base_dir, 'videopoker_hs.pkl'),
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
for file in databaseFiles:
try:
with open(file, 'rb') as f:
if 'lemonstand' in file:
lemon_score = pickle.load(f)
elif 'dopewar' in file:
dopewar_score = pickle.load(f)
elif 'blackjack' in file:
blackjack_score = pickle.load(f)
elif 'videopoker' in file:
videopoker_score = pickle.load(f)
elif 'mmind' in file:
mmind_score = pickle.load(f)
elif 'golfsim' in file:
golfsim_score = pickle.load(f)
elif 'bbsdb' in file:
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
lemon_score = "no data"
elif 'dopewar' in file:
dopewar_score = "no data"
elif 'blackjack' in file:
blackjack_score = "no data"
elif 'videopoker' in file:
videopoker_score = "no data"
elif 'mmind' in file:
mmind_score = "no data"
elif 'golfsim' in file:
golfsim_score = "no data"
elif 'bbsdb' in file:
bbsdb = "no data"
elif 'bbsdm' in file:
bbsdm = "no data"
# pretty print the bbsdb
prettyBBSdb = ""
try:
for i in range(len(bbsdb)):
prettyBBSdb += f'<li>{bbsdb[i]}</li>'
except Exception as e:
print(f"Error with database: {str(e)}")
pass
# pretty print the bbsdm
prettyBBSdm = ""
try:
for i in range(len(bbsdm)):
prettyBBSdm += f'<li>{bbsdm[i]}</li>'
except Exception as e:
print(f"Error with database: {str(e)}")
pass
if 'no data' in [lemon_score, dopewar_score, blackjack_score, videopoker_score, mmind_score, golfsim_score]:
database = "Error(s) Detected"
else:
database = " Online"
return {
'database': database,
"bbsdb": prettyBBSdb,
"bbsdm": prettyBBSdm,
'lemon_score': lemon_score,
'dopewar_score': dopewar_score,
'blackjack_score': blackjack_score,
'videopoker_score': videopoker_score,
'mmind_score': mmind_score,
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
}
def generate_main_html(log_data, system_info):
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MeshBot (BBS) Web Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f0f0;
display: flex;
}
.header {
background-color: #333;
color: white;
padding: 10px;
font-size: 24px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.sidebar {
width: 200px;
background-color: #ddd;
padding: 10px;
height: 100vh;
position: fixed;
top: 50px;
left: 0;
overflow-y: auto;
}
.content {
margin-left: 220px;
margin-top: 60px;
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
flex-grow: 1;
}
.chart-container {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
height: 400px;
display: flex;
flex-direction: column;
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
#map, .chart-content {
flex-grow: 1;
width: 100%;
}
.list-container {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
height: 400px;
overflow-y: auto;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
li:last-child {
border-bottom: none;
}
#iframe-content {
display: none;
position: fixed;
top: 50px;
left: 220px;
right: 0;
bottom: 0;
background: white;
z-index: 900;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
.timestamp-list {
height: 400px;
overflow-y: auto;
font-size: 12px;
}
</style>
</head>
<body>
<div class="header">MeshBot (BBS) Web Dashboard</div>
<div class="sidebar">
<ul>
<li><a href="#" onclick="showDashboard(); return false;">Dashboard</a></li>
<li><a href="#" onclick="showIframe('network_map_${date}.html'); return false;">Network Map</a></li>
<li><a href="#" onclick="showIframe('wall_of_shame_${date}.html'); return false;">Wall of Shame</a></li>
<li><a href="#" onclick="showIframe('database_${date}.html'); return false;">Database</a></li>
<li><a href="#" onclick="showIframe('hosts_${date}.html'); return false;">System Host</a></li>
</ul>
</div>
<div class="content" id="dashboard-content">
<div class="chart-container">
<div class="chart-title">Node Locations</div>
<div id="map"></div>
</div>
<div class="chart-container">
<div class="chart-title">Network Activity</div>
<div class="chart-content">
<canvas id="activityChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Command Usage</div>
<div class="chart-content">
<canvas id="commandChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Message Types</div>
<div class="chart-content">
<canvas id="messageChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">BBS Stored Message Counts</div>
<div class="chart-content">
<canvas id="messageCountChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Recent Commands</div>
<div class="timestamp-list">
<ul>
${command_timestamps}
</ul>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Recent Messages</div>
<div class="timestamp-list">
<ul>
${message_timestamps}
</ul>
</div>
</div>
<div class="list-container">
<div class="chart-title">Unique Users</div>
<ul>
${unique_users}
</ul>
</div>
<div class="list-container">
<div class="chart-title">Warnings</div>
<ul>
${warnings}
</ul>
</div>
<div class="list-container">
<div class="chart-title">Errors</div>
<ul>
${errors}
</ul>
</div>
</div>
<div id="iframe-content">
<iframe id="content-iframe" src=""></iframe>
</div>
<script>
const commandData = ${command_data};
const messageData = ${message_data};
const activityData = ${activity_data};
const messageCountData = {
labels: ['BBSdm Messages', 'BBSdb Messages', 'Channel Messages'],
datasets: [{
label: 'Message Counts',
data: [${messages_waiting}, ${bbs_messages}, ${total_messages}],
backgroundColor: ['rgba(255, 206, 86, 0.6)', 'rgba(75, 192, 192, 0.6)', 'rgba(54, 162, 235, 0.6)']
}]
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false
};
new Chart(document.getElementById('commandChart'), {
type: 'bar',
data: {
labels: Object.keys(commandData),
datasets: [{
label: 'Command Usage',
data: Object.values(commandData),
backgroundColor: 'rgba(75, 192, 192, 0.6)'
}]
},
options: chartOptions
});
new Chart(document.getElementById('messageChart'), {
type: 'pie',
data: {
labels: Object.keys(messageData),
datasets: [{
data: Object.values(messageData),
backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)']
}]
},
options: chartOptions
});
new Chart(document.getElementById('activityChart'), {
type: 'line',
data: {
labels: Object.keys(activityData),
datasets: [{
label: 'Hourly Activity',
data: Object.entries(activityData).map(([time, count]) => ({x: new Date(time), y: count})),
borderColor: 'rgba(153, 102, 255, 1)',
fill: false
}]
},
options: {
...chartOptions,
scales: {
x: {
type: 'time',
time: {
unit: 'hour',
displayFormats: {
hour: 'MMM d, HH:mm'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Activity Count'
}
}
}
}
});
new Chart(document.getElementById('messageCountChart'), {
type: 'bar',
data: messageCountData,
options: {
...chartOptions,
scales: {
y: {
beginAtZero: true
}
}
}
});
var map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var gpsCoordinates = ${gps_coordinates};
for (var nodeId in gpsCoordinates) {
var coords = gpsCoordinates[nodeId][0];
L.marker(coords).addTo(map)
.bindPopup("Node ID: " + nodeId);
}
var bounds = [];
for (var nodeId in gpsCoordinates) {
bounds.push(gpsCoordinates[nodeId][0]);
}
map.fitBounds(bounds);
function showIframe(src) {
document.getElementById('dashboard-content').style.display = 'none';
document.getElementById('iframe-content').style.display = 'block';
document.getElementById('content-iframe').src = src;
}
function showDashboard() {
document.getElementById('dashboard-content').style.display = 'grid';
document.getElementById('iframe-content').style.display = 'none';
document.getElementById('content-iframe').src = '';
}
</script>
</body>
</html>
"""
template = Template(html_template)
return template.safe_substitute(
date=datetime.now().strftime('%Y-%m-%d'),
command_data=json.dumps(log_data['command_counts']),
message_data=json.dumps(log_data['message_types']),
activity_data=json.dumps(log_data['hourly_activity']),
bbs_messages=log_data['bbs_messages'],
messages_waiting=log_data['messages_waiting'],
total_messages=log_data['total_messages'],
total_llm_queries=log_data['message_types']['LLM Query'],
gps_coordinates=json.dumps(log_data['gps_coordinates']),
unique_users='\n'.join(f'<li>{user}</li>' for user in log_data['unique_users']),
warnings='\n'.join(f'<li>{warning}</li>' for warning in log_data['warnings']),
errors='\n'.join(f'<li>{error}</li>' for error in log_data['errors']),
command_timestamps='\n'.join(f'<li>{timestamp}: {cmd}</li>' for timestamp, cmd in reversed(log_data['command_timestamps'][-50:])),
message_timestamps='\n'.join(f'<li>{timestamp}: {msg_type}</li>' for timestamp, msg_type in reversed(log_data['message_timestamps'][-50:]))
)
def generate_network_map_html(log_data):
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Network Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var gpsCoordinates = ${gps_coordinates};
for (var nodeId in gpsCoordinates) {
var coords = gpsCoordinates[nodeId][0];
L.marker(coords).addTo(map)
.bindPopup("Node ID: " + nodeId);
}
var bounds = [];
for (var nodeId in gpsCoordinates) {
bounds.push(gpsCoordinates[nodeId][0]);
}
map.fitBounds(bounds);
</script>
</body>
</html>
"""
template = Template(html_template)
return template.safe_substitute(gps_coordinates=json.dumps(log_data['gps_coordinates']))
def generate_sys_hosts_html(system_info):
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Host Information</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
h1 { color: #00ff00; }
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>System Host Information</h1>
<table>
<tr><th>OS Metric</th><th>Value</th></tr>
<tr><td>Uptime</td><td>${uptime}</td></tr>
<tr><td>Total Memory</td><td>${memory_total}</td></tr>
<tr><td>Available Memory</td><td>${memory_available}</td></tr>
<tr><td>Total Disk Space</td><td>${disk_total}</td></tr>
<tr><td>Free Disk Space</td><td>${disk_free}</td></tr>
<tr><th>Meshtastic Metric</th><th>Value</th></tr>
<tr><td>API Version/Latest</td><td>${cli_local} / ${cli_web}</td></tr>
<tr><td>Int1 Name ID</td><td>${node1_name} (${node1_ID})</td></tr>
<tr><td>Int1 Stat</td><td>${node1_uptime}</td></tr>
<tr><td>Int1 FW Version</td><td>${interface1_version}</td></tr>
<tr><td>Int2 Name ID</td><td>${node2_name} (${node2_ID})</td></tr>
<tr><td>Int2 Stat</td><td>${node2_uptime}</td></tr>
<tr><td>Int2 FW Version</td><td>${interface2_version}</td></tr>
</table>
</body>
</html>
"""
template = Template(html_template)
return template.safe_substitute(system_info)
def generate_wall_of_shame_html(shame_info):
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wall Of Shame</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
h1 { color: #00ff00; }
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Collected Shame</h1>
<table>
<tr><th>Shame Metric</th><th>Value</th></tr>
<tr><td>Shamefull words</td><td>${shame}</td></tr>
<tr><td>Shamefull messages</td><td>${shameList}</td></tr>
</table>
</body>
</html>
"""
template = Template(html_template)
return template.safe_substitute(shame_info)
def generate_database_html(database_info):
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Information</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
h1 { color: #00ff00; }
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Database Information</h1>
<p>Connection ${database}</p>
<table>
<tr><th>config.ini Settings</th><th>Value</th></tr>
<tr><td>Admin List</td><td>${adminList}</td></tr>
<tr><td>Ban List</td><td>${banList}</td></tr>
<tr><td>Sentry Ignore List</td><td>${sentryIgnoreList}</td></tr>
</table>
<h1>BBS Message Database</h1>
<p>BBSdb: ${bbsdb}</p>
<p>BBSdm: ${bbsdm}</p>
<h1>High Scores</h1>
<table>
<tr><th>Game</th><th>High Score</th></tr>
<tr><td>Lemonade Stand</td><td>${lemon_score}</td></tr>
<tr><td>Dopewars</td><td>${dopewar_score}</td></tr>
<tr><td>Blackjack</td><td>${blackjack_score}</td></tr>
<tr><td>Video Poker</td><td>${videopoker_score}</td></tr>
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
</body>
</html>
"""
template = Template(html_template)
return template.safe_substitute(database_info)
def main():
log_dir = LOG_PATH
today = datetime.now().strftime('%Y-%m-%d')
log_file = f'meshbot.log'
log_path = os.path.join(log_dir, log_file)
if not os.path.exists(log_path):
# set file_path to the cwd of the default project ../log
file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
file_path = os.path.abspath(file_path)
log_path = os.path.join(file_path, log_file)
log_data = parse_log_file(log_path)
system_info = get_system_info()
shame_info = get_wall_of_shame()
database_info = get_database_info()
main_html = generate_main_html(log_data, system_info)
network_map_html = generate_network_map_html(log_data)
hosts_html = generate_sys_hosts_html(system_info)
wall_of_shame = generate_wall_of_shame_html(shame_info)
database_html = generate_database_html(database_info)
output_dir = W3_PATH
index_path = os.path.join(output_dir, 'index.html')
print(f"\n\nMeshBot (BBS) Web Dashboard Report Generator")
print(f"\nMain dashboard: file://{index_path}\n")
try:
if not os.path.exists(output_dir):
output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'www')
output_dir = os.path.abspath(output_dir)
index_path = os.path.join(output_dir, 'index.html')
# Create backup of existing index.html if it exists
if os.path.exists(index_path):
backup_path = os.path.join(output_dir, f'index_backup_{today}.html')
os.rename(index_path, backup_path)
print(f"Existing index.html backed up to {backup_path}")
# Write main HTML to index.html
with open(index_path, 'w') as f:
f.write(main_html)
print(f"Main dashboard written to {index_path}")
# Write other HTML files
with open(os.path.join(output_dir, f'network_map_{today}.html'), 'w') as f:
f.write(network_map_html)
with open(os.path.join(output_dir, f'hosts_{today}.html'), 'w') as f:
f.write(hosts_html)
with open(os.path.join(output_dir, f'wall_of_shame_{today}.html'), 'w') as f:
f.write(wall_of_shame)
with open(os.path.join(output_dir, f'database_{today}.html'), 'w') as f:
f.write(database_html)
print(f"HTML reports generated for {today} in {output_dir}")
except PermissionError:
print("Error: Permission denied. Please run the script with appropriate permissions (e.g., using sudo).")
except Exception as e:
print(f"An error occurred while writing the output: {str(e)}")
if __name__ == "__main__":
main()

1285
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

@@ -1,6 +1,6 @@
#!/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 * # Import the logger
import time
import random
@@ -25,7 +25,7 @@ def get_name_from_number(nodeID, length='short', interface=1):
# # Function to handle, or the project in test
def example_handler(nodeID, message):
def example_handler(message, nodeID, deviceID):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "
@@ -42,7 +42,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](0, 0, " ") # Call the project handler under test
projectResponse = globals()[projectName]("", nodeID, nodeInt) # Call the project handler under test
while True: # represents the onReceive() loop in the bot.py
projectResponse = ""
responseLength = 0
@@ -51,7 +51,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, deviceID=nodeInt, message=packet) # Call the project handler under test
projectResponse = globals()[projectName](message = packet, nodeID = nodeID, deviceID = nodeInt)
# 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

@@ -5,6 +5,7 @@ cd "$(dirname "$0")"
program_path=$(pwd)
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
printf "\nMeshing Around Installer\n"
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
@@ -58,7 +59,7 @@ if [ $venv == "y" ]; then
else
sudo apt-get install python3-venv
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
exxt 1
exit 1
fi
# config service files for virtual environment
@@ -90,14 +91,17 @@ read bot
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
# set the correct user in the service file?
whoami=$(whoami)
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
replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sudo systemctl daemon-reload
printf "\n service files updated\n"

View File

@@ -3,22 +3,31 @@
# 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
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
else
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5)"
exit 1
fi
deactivate

View File

@@ -1,15 +1,26 @@
Logs will collect here.
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
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.
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed.
- `multi_log_reader = True` on by default will read all logs (or set to false to return daily logs)
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
- provided serviceTimer templates in etc/
![reportView](../etc/reporting.jpg)
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
```
[general]
# logging to file of the non Bot messages
LogMessagesToFile = True
# Logging of system messages to file
# logging to file of the non Bot messages only
LogMessagesToFile = False
# Logging of system messages to file, needed for reporting engine
SyslogToFile = True
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
Example to log to disk only INFO and higher (ignore DEBUG)
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
```
*log.py
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
# Set level for stdout handler
stdout_handler.setLevel(logging.INFO)
```

View File

@@ -25,47 +25,57 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# Command List
default_commands = {
"ping": lambda: handle_ping(message, hop, snr, rssi, isDM),
"pong": lambda: "🏓PING!!",
"motd": lambda: handle_motd(message, message_from_id, isDM),
"bbshelp": bbs_help,
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
"wiki:": lambda: handle_wiki(message, isDM),
"wiki?": lambda: handle_wiki(message, isDM),
"games": lambda: gamesCmdList,
"dopewars": lambda: handleDopeWars(message_from_id, message, deviceID),
"lemonstand": lambda: handleLemonade(message_from_id, message),
"blackjack": lambda: handleBlackJack(message_from_id, message),
"videopoker": lambda: handleVideoPoker(message_from_id, message),
"mastermind": lambda: handleMmind(message_from_id, deviceID, message),
"golfsim": lambda: handleGolf(message_from_id, message),
"globalthermonuclearwar": lambda: handle_gTnW(),
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"joke": tell_joke,
"bbsack": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"bbshelp": bbs_help,
"bbsinfo": lambda: get_bbs_stats(),
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbslist": bbs_list_messages,
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
"bbsread": lambda: handle_bbsread(message),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"bbsinfo": lambda: get_bbs_stats(),
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
"blackjack": lambda: handleBlackJack(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),
"cmd": lambda: help_message,
"history": lambda: handle_history(message, message_from_id, deviceID, isDM),
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"games": lambda: gamesCmdList,
"globalthermonuclearwar": lambda: handle_gTnW(),
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
"hfcond": hf_band_conditions,
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"history": lambda: handle_history(message, message_from_id, deviceID, isDM),
"joke": lambda: tell_joke(message_from_id),
"lemonstand": lambda: handleLemonade(message, message_from_id, deviceID),
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"mastermind": lambda: handleMmind(message, message_from_id, deviceID),
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
"ack": lambda: handle_ping(message, hop, snr, rssi, isDM),
"testing": lambda: handle_ping(message, hop, snr, rssi, isDM),
"test": lambda: handle_ping(message, hop, snr, rssi, isDM),
"motd": lambda: handle_motd(message, message_from_id, isDM),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"readnews": lambda: read_news(),
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
"wiki:": lambda: handle_wiki(message, isDM),
"wiki?": lambda: handle_wiki(message, isDM),
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
"📍": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
"🔔": lambda: handle_alertBell(message_from_id, deviceID, message),
}
# set the command handler
@@ -85,7 +95,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
if len(cmds) > 0:
# sort the commands by index value
cmds = sorted(cmds, key=lambda k: k['index'])
logger.debug(f"System: Bot detected Commands:{cmds}")
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
# check the command isnt a isDM only command
if cmds[0]['cmd'] in restrictedCommands and not isDM:
bot_response = restrictedResponse
@@ -99,26 +109,36 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
def handle_ping(message, hop, snr, rssi, isDM):
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
msg = ""
type = ''
if "ping" in message.lower():
msg = "🏓PONG, "
msg = "🏓PONG\n"
type = "🏓PING"
elif "test" in message.lower() or "testing" in message.lower():
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing, ",\
"🎙Testing, testing, ",\
"🎙Ah-wun, ah-two... ", "🎙Is this thing on? ",\
"🎙Roger that! ",])
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
"🎙Testing, testing\n",\
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
"🎙Roger that!\n",])
type = "🎙TEST"
elif "ack" in message.lower():
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
msg = f"QSP QSL OM DE {myname} K\n"
else:
msg = ""
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
@@ -127,11 +147,48 @@ def handle_ping(message, hop, snr, rssi, isDM):
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
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
return msg
def handle_alertBell(message_from_id, deviceID, message):
msg = ["the only prescription is more 🐮🔔🐄🛎️", "what this 🤖 needs is more 🐮🔔🐄🛎️", "🎤ring my bell🛎🔔🎶"]
return random.choice(msg)
def handle_motd(message, message_from_id, isDM):
global MOTD
isAdmin = False
@@ -170,7 +227,9 @@ def handle_wxalert(message_from_id, deviceID, message):
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
if NO_ALERTS not in weatherAlert:
weatherAlert = weatherAlert[0]
return weatherAlert
def handle_wiki(message, isDM):
@@ -279,7 +338,7 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
return response
def handleDopeWars(nodeID, message, rxNode):
def handleDopeWars(message, nodeID, rxNode):
global dwPlayerTracker, dwHighScore
# get player's last command
@@ -295,10 +354,10 @@ def handleDopeWars(nodeID, message, rxNode):
msg += 'The High Score is $' + "{:,}".format(high_score.get('cash')) + ' by user ' + get_name_from_number(high_score.get('userID') , 'short', rxNode) +'\n'
msg += playDopeWars(nodeID, message)
else:
logger.debug("System: DopeWars: last_cmd: " + str(last_cmd))
logger.debug(f"System: {nodeID} PlayingGame dopewars last_cmd: {last_cmd}")
msg = playDopeWars(nodeID, message)
# wait a second to keep from message collision
time.sleep(1)
time.sleep(responseDelay + 1)
return msg
def handle_gTnW():
@@ -316,7 +375,7 @@ def handle_gTnW():
selected_index = random.choice(indices)
return response[selected_index]
def handleLemonade(nodeID, message):
def handleLemonade(message, nodeID, deviceID):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
msg = ""
def create_player(nodeID):
@@ -327,13 +386,15 @@ def handleLemonade(nodeID, message):
lemonadeLemons.append({'nodeID': nodeID, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00})
lemonadeSugar.append({'nodeID': nodeID, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00})
lemonadeScore.append({'nodeID': nodeID, 'value': 0.00, 'total': 0.00})
lemonadeWeeks.append({'nodeID': nodeID, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00})
lemonadeWeeks.append({'nodeID': nodeID, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0})
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
last_cmd = lemonadeTracker[i]['cmd']
logger.debug(f"System: {nodeID} PlayingGame lemonstand last_cmd: {last_cmd}")
# create new player if not in tracker
if last_cmd == "" and nodeID != 0:
create_player(nodeID)
@@ -347,14 +408,14 @@ def handleLemonade(nodeID, message):
nodeName = get_name_from_number(highScore['userID'])
if nodeName.isnumeric() and interface2_enabled:
nodeName = get_name_from_number(highScore['userID'], 'long', 2)
msg += f" HighScore🥇{nodeName} 💰{highScore['cash']}k "
msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k "
msg += start_lemonade(nodeID=nodeID, message=message, celsius=False)
# wait a second to keep from message collision
time.sleep(1)
time.sleep(responseDelay + 1)
return msg
def handleBlackJack(nodeID, message):
def handleBlackJack(message, nodeID, deviceID):
global jackTracker
msg = ""
@@ -378,7 +439,7 @@ def handleBlackJack(nodeID, message):
msg = playBlackJack(nodeID=nodeID, message=message)
if last_cmd != "" and nodeID != 0:
logger.debug(f"System: BlackJack: {nodeID} last command: {last_cmd}")
logger.debug(f"System: {nodeID} PlayingGame blackjack last_cmd: {last_cmd}")
else:
highScore = {'nodeID': 0, 'highScore': 0}
highScore = loadHSJack()
@@ -388,10 +449,10 @@ def handleBlackJack(nodeID, message):
if nodeName.isnumeric() and interface2_enabled:
nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} chips. "
time.sleep(1.5) # short answers with long replies can cause message collision added wait
time.sleep(responseDelay + 1) # short answers with long replies can cause message collision added wait
return msg
def handleVideoPoker(nodeID, message):
def handleVideoPoker(message, nodeID, deviceID):
global vpTracker
msg = ""
@@ -425,11 +486,11 @@ def handleVideoPoker(nodeID, message):
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} coins. "
if last_cmd != "" and nodeID != 0:
logger.debug(f"System: VideoPoker: {nodeID} last command: {last_cmd}")
time.sleep(1.5) # short answers with long replies can cause message collision added wait
logger.debug(f"System: {nodeID} PlayingGame videopoker last_cmd: {last_cmd}")
time.sleep(responseDelay + 1) # short answers with long replies can cause message collision added wait
return msg
def handleMmind(nodeID, deviceID, message):
def handleMmind(message, nodeID, deviceID):
global mindTracker
msg = ''
@@ -451,6 +512,8 @@ def handleMmind(nodeID, deviceID, message):
if mindTracker[i]['nodeID'] == nodeID:
last_cmd = mindTracker[i]['cmd']
logger.debug(f"System: {nodeID} PlayingGame mastermind last_cmd: {last_cmd}")
if last_cmd == "" and nodeID != 0:
# create new player
logger.debug("System: MasterMind: New Player: " + str(nodeID))
@@ -462,10 +525,10 @@ def handleMmind(nodeID, deviceID, message):
msg += start_mMind(nodeID=nodeID, message=message)
# wait a second to keep from message collision
time.sleep(1.5)
time.sleep(responseDelay + 1)
return msg
def handleGolf(nodeID, message):
def handleGolf(message, nodeID, deviceID):
global golfTracker
msg = ''
@@ -483,6 +546,8 @@ def handleGolf(nodeID, message):
golfTracker.pop(i)
return msg
logger.debug(f"System: {nodeID} PlayingGame golfsim last_cmd: {last_cmd}")
if last_cmd == "" and nodeID != 0:
# create new player
logger.debug("System: GolfSim: New Player: " + str(nodeID))
@@ -492,7 +557,7 @@ def handleGolf(nodeID, message):
msg += playGolf(nodeID=nodeID, message=message)
# wait a second to keep from message collision
time.sleep(1.5)
time.sleep(responseDelay + 1)
return msg
def handle_wxc(message_from_id, deviceID, cmd):
@@ -581,46 +646,19 @@ 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"
else:
# display last heard nodes add to response
bot_response = str(get_node_list(1))
# gather telemetry
chutil1 = round(interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
airUtilTx = round(interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
uptimeSeconds = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0)
batteryLevel = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("batteryLevel", 0)
voltage = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("voltage", 0)
if interface2_enabled:
bot_response += "P2:\n" + str(get_node_list(2))
chutil2 = round(interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
airUtilTx2 = round(interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
uptimeSeconds2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0)
batteryLevel2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("batteryLevel", 0)
voltage2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("voltage", 0)
else:
chutil2, airUtilTx2, uptimeSeconds2, batteryLevel2, voltage2 = 0, 0, 0, 0, 0
# add the channel utilization and airUtilTx to the bot response
bot_response += "\nUse/Tx " + str(chutil1) + "%" + "/" + str(airUtilTx) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%" + "/" + str(airUtilTx2) + "%"
# convert uptime to minutes, hours, or days
uptimeSeconds = getPrettyTime(uptimeSeconds)
uptimeSeconds2 = getPrettyTime(uptimeSeconds2)
# add uptime and battery info to the bot response
bot_response += "\nUptime:" + str(uptimeSeconds)
if interface2_enabled:
bot_response += f" P2:" + {uptimeSeconds2}
# add battery info to the bot response
emji = "🔌" if batteryLevel == 101 else "🪫" if batteryLevel < 10 else "🔋"
emji2 = "🔌" if batteryLevel2 == 101 else "🪫" if batteryLevel2 < 10 else "🔋"
if not batteryLevel == 101:
bot_response += f" {emji}{batteryLevel}% Volt:{voltage}"
if interface2_enabled and not batteryLevel2 == 101:
bot_response += f" P2:{emji2}{batteryLevel2}% Volt:{voltage2}"
# display last heard nodes add to response
bot_response = "Last Heard\n"
bot_response += str(get_node_list(1))
# show last users of the bot with the cmdHistory list
history = handle_history(message, nodeid, deviceID, isDM, lheard=True)
if history:
bot_response += f'\n{history}'
bot_response += f'LastSeen\n{history}'
else:
# trim the last \n
bot_response = bot_response[:-1]
# bot_response += getNodeTelemetry(deviceID)
return bot_response
def handle_history(message, nodeid, deviceID, isDM, lheard=False):
@@ -631,8 +669,8 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of commands received."
# show the last commands from the user to the bot
elif not lheard:
# show the last commands from the user to the bot
if not lheard:
for i in range(len(cmdHistory)):
cmdTime = round((time.time() - cmdHistory[i]['time']) / 600) * 5
prettyTime = getPrettyTime(cmdTime)
@@ -680,6 +718,15 @@ def handle_whereami(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return where_am_i(str(location[0]), str(location[1]))
def handle_repeaterQuery(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
if repeater_lookup == "rbook":
return getRepeaterBook(str(location[0]), str(location[1]))
elif repeater_lookup == "artsci":
return getArtSciRepeaters(str(location[0]), str(location[1]))
else:
return "Repeater lookup not enabled"
def handle_tide(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_tide(str(location[0]), str(location[1]))
@@ -690,160 +737,77 @@ def handle_moon(message_from_id, deviceID, channel_number):
def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
loc = []
msg = "You are " + str(message_from_id) + " AKA " +\
str(get_name_from_number(message_from_id, 'long', deviceID) + " AKA, " +\
str(get_name_from_number(message_from_id, 'short', deviceID)) + " AKA, " +\
str(decimal_to_hex(message_from_id)) + f"\n")
msg += f"I see the signal strength is {rssi} and the SNR is {snr} with hop count of {hop}"
if pkiStatus[1] != 'ABC':
msg += f"\nYour PKI bit is {pkiStatus[0]} pubKey: {pkiStatus[1]}"
loc = get_node_location(message_from_id, deviceID)
if loc != [latitudeValue,longitudeValue]:
msg += f"\nYou are at: lat:{loc[0]} lon:{loc[1]}"
try:
loc = []
msg = "You are " + str(message_from_id) + " AKA " +\
str(get_name_from_number(message_from_id, 'long', deviceID) + " AKA, " +\
str(get_name_from_number(message_from_id, 'short', deviceID)) + " AKA, " +\
str(decimal_to_hex(message_from_id)) + f"\n")
msg += f"I see the signal strength is {rssi} and the SNR is {snr} with hop count of {hop}"
if pkiStatus[1] != 'ABC':
msg += f"\nYour PKI bit is {pkiStatus[0]} pubKey: {pkiStatus[1]}"
loc = get_node_location(message_from_id, deviceID)
if loc != [latitudeValue, longitudeValue]:
msg += f"\nYou are at: lat:{loc[0]} lon:{loc[1]}"
# check the positionMetadata for nodeID and get metadata
if positionMetadata and message_from_id in positionMetadata:
metadata = positionMetadata[message_from_id]
msg += f" alt:{metadata.get('altitude')}, speed:{metadata.get('groundSpeed')} bit:{metadata.get('precisionBits')}"
except Exception as e:
logger.error(f"System: Error in whoami: {e}")
msg = "Error in whoami"
return msg
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
global llm_enabled
for i in range(len(tracker)):
if tracker[i].get('nodeID') == message_from_id or tracker[i].get('userID') == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key) > (time.time() - GAMEDELAY):
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
# play the game
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
return True, game_name
else:
# pop if the time exceeds 8 hours
tracker.pop(i)
return False, game_name
return False, "None"
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
# Checks if in a game used for, LLM disable for duration of game plays the game.
# Also handles stale games and resets the player if the game is older than 8 hours
playingGame = False
game = "None"
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == message_from_id:
# check if the player has played in the last 8 hours
if dwPlayerTracker[i].get('last_played') > (time.time() - GAMEDELAY):
playingGame = True
game = "DopeWars"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of Dope Wars")
#if time exceeds 8 hours reset the player
if dwPlayerTracker[i].get('last_played') < (time.time() - GAMEDELAY):
logger.debug(f"System: DopeWars: Resetting player {message_from_id}")
dwPlayerTracker.pop(i)
# play the game
send_message(handleDopeWars(message_from_id, message_string, rxNode), channel_number, message_from_id, rxNode)
for i in range(0, len(lemonadeTracker)):
if lemonadeTracker[i].get('nodeID') == message_from_id:
# check if the player has played in the last 8 hours
if lemonadeTracker[i].get('time') > (time.time() - GAMEDELAY):
playingGame = True
game = "LemonadeStand"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of Lemonade Stand")
trackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars),
(lemonadeTracker, "LemonadeStand", handleLemonade),
(vpTracker, "VideoPoker", handleVideoPoker),
(jackTracker, "BlackJack", handleBlackJack),
(mindTracker, "MasterMind", handleMmind),
(golfTracker, "GolfSim", handleGolf),
]
#if time exceeds 8 hours reset the player
if lemonadeTracker[i].get('time') < (time.time() - GAMEDELAY):
logger.debug(f"System: LemonadeStand: Resetting player {message_from_id}")
lemonadeTracker.pop(i)
# play the game
send_message(handleLemonade(message_from_id, message_string), channel_number, message_from_id, rxNode)
for tracker, game_name, handle_game_func in trackers:
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
if playingGame:
break
for i in range(0, len(vpTracker)):
if vpTracker[i].get('nodeID') == message_from_id:
# check if the player has played in the last 8 hours
if vpTracker[i].get('time') > (time.time() - GAMEDELAY):
playingGame = True
game = "VideoPoker"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of VideoPoker")
# play the game
send_message(handleVideoPoker(message_from_id, message_string), channel_number, message_from_id, rxNode)
else:
# pop if the time exceeds 8 hours
vpTracker.pop(i)
for i in range(0, len(jackTracker)):
if jackTracker[i].get('nodeID') == message_from_id:
# check if the player has played in the last 8 hours
if jackTracker[i].get('time') > (time.time() - GAMEDELAY):
playingGame = True
game = "BlackJack"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of BlackJack")
# play the game
send_message(handleBlackJack(message_from_id, message_string), channel_number, message_from_id, rxNode)
else:
# pop if the time exceeds 8 hours
jackTracker.pop(i)
for i in range(0, len(mindTracker)):
if mindTracker[i].get('nodeID') == message_from_id:
# check if the player has played in the last 8 hours
if mindTracker[i].get('last_played') > (time.time() - GAMEDELAY):
playingGame = True
game = "MasterMind"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of MasterMind")
# play the game
send_message(handleMmind(message_from_id, rxNode, message_string), channel_number, message_from_id, rxNode)
else:
# pop if the time exceeds 8 hours
mindTracker.pop(i)
for i in range(0, len(golfTracker)):
if golfTracker[i].get('nodeID') == message_from_id:
# check if the player has played in the last 8 hours
if golfTracker[i].get('last_played') > (time.time() - GAMEDELAY):
playingGame = True
game = "GolfSim"
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of GolfSim")
# play the game
send_message(handleGolf(message_from_id, message_string), channel_number, message_from_id, rxNode)
else:
# pop if the time exceeds 8 hours
golfTracker.pop(i)
#logger.debug(f"System: {message_from_id} is playing {game}")
return playingGame
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical("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("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("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):
# Priocess the incoming packet, handles the responses to the packet
# extract interface defailts from interface object
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
# extract interface details from inbound packet
rxType = type(interface).__name__
rxNode = 0
message_from_id = 0
snr = 0
rssi = 0
hop = 0
hop_away = 0
# Valies assinged to the packet
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
pkiStatus = (False, 'ABC')
isDM = False
@@ -854,6 +818,8 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface:
@@ -874,14 +840,13 @@ def onReceive(packet, interface):
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# check for BBS DM for mail delivery
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# BBS DM MAIL CHECKER
if bbs_enabled and 'decoded' in packet:
message_from_id = packet['from']
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
msg = bbs_check_dm(message_from_id)
if msg:
@@ -892,7 +857,7 @@ def onReceive(packet, interface):
bbs_delete_dm(msg[0], msg[1])
send_message(message, channel_number, message_from_id, rxNode)
# check for a message packet and process it
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
@@ -904,10 +869,6 @@ def onReceive(packet, interface):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
@@ -929,6 +890,7 @@ def onReceive(packet, interface):
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
@@ -939,7 +901,7 @@ def onReceive(packet, interface):
hop = f"{hop_count} hops"
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
@@ -949,7 +911,7 @@ def onReceive(packet, interface):
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
# log the message to the message log
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -957,18 +919,28 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# DM is useful for games or LLM
if games_enabled:
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
else:
if games_enabled:
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
playingGame = False
if not playingGame:
if llm_enabled:
# respond with LLM
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
@@ -979,7 +951,7 @@ def onReceive(packet, interface):
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
else:
# 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 +\
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
@@ -988,13 +960,14 @@ def onReceive(packet, interface):
# 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)}")
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
@@ -1028,9 +1001,12 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
logger.critical(f"System: Packet: {packet}")
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)
@@ -1052,6 +1028,11 @@ async def start_rx():
logger.debug("System: Logging System Logs to disk")
if bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if bbs_link_enabled:
if len(bbs_link_whitelist) > 0:
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
else:
logger.debug(f"System: BBS Link Enabled allowing all")
if solar_conditions_enabled:
logger.debug("System: Celestial Telemetry Enabled")
if location_enabled:
@@ -1077,9 +1058,18 @@ async def start_rx():
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if file_monitor_enabled:
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
if read_news_enabled:
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
if wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Reminder Scheduler is enabled every Monday at noon send a log message
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
@@ -1096,13 +1086,17 @@ async def start_rx():
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send a joke every 2 minutes using tell_joke function to channel 2 on device 1
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
#
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
@@ -1115,11 +1109,15 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if file_monitor_enabled:
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:
await asyncio.wait([meshRxTask, watchdogTask])
await asyncio.gather(meshRxTask, watchdogTask)
await asyncio.gather(hamlibTask)
await asyncio.gather(fileMonTask)
await asyncio.sleep(0.01)
try:

View File

@@ -3,8 +3,9 @@
import pickle # pip install pickle
from modules.log import *
import time
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo")
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
# global message list, later we will use a pickle on disk
bbs_messages = []
@@ -14,19 +15,19 @@ def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
try:
with open('bbsdb.pkl', 'rb') as f:
with open('data/bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except Exception as e:
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:
logger.debug("System: Creating new data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
def save_bbsdb():
global bbs_messages
# save the bbs messages to the database file
logger.debug("System: Saving bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
logger.debug("System: Saving data/bbsdb.pkl")
with open('data/bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
def bbs_help():
@@ -77,6 +78,12 @@ def bbs_post_message(subject, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# validate not a duplicate message
for msg in bbs_messages:
if msg[1].strip().lower() == subject.strip().lower() and msg[2].strip().lower() == message.strip().lower():
messageID = msg[0]
return "Message posted. ID is: " + str(messageID)
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
@@ -100,20 +107,20 @@ def bbs_read_message(messageID = 0):
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:
with open('data/bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
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):
@@ -133,7 +140,7 @@ def bbs_post_dm(toNode, message, fromNode):
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. Direct ✉️ Messages waiting: {(len(bbs_dm) - 1)}"
return f"📡BBSdb has {len(bbs_messages)} messages.\nDirect ✉️ Messages waiting: {(len(bbs_dm) - 1)}"
def bbs_check_dm(toNode):
global bbs_dm
@@ -156,6 +163,42 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# check if the bbs link is enabled
if bbs_link_whitelist is not None:
if str(peerNode) not in bbs_link_whitelist:
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
return "System: BBS Link is disabled for your node."
if bbs_link_enabled == False:
return "System: BBS Link is disabled."
# respond when another bot asks for the bbs posts to sync
if "bbslink" in input.lower():
if "$" in input and "#" in input:
#store the message
subject = input.split("$")[1].split("#")[0]
body = input.split("#")[1]
bbs_post_message(subject, body, peerNode)
messageID = input.split(" ")[1]
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
# send message with delay to keep chutil happy
if messageID < len(bbs_messages):
time.sleep(5 + responseDelay)
# every 5 messages add extra delay
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
#initialize the bbsdb's
load_bbsdb()
load_bbsdm()

39
modules/filemon.py Normal file
View File

@@ -0,0 +1,39 @@
# File monitor module for the meshing-around bot
# 2024 Kelly Keeton K7MHI
from modules.log import *
import asyncio
import os
trap_list_filemon = ("readnews",)
def read_file(file_monitor_file_path):
try:
with open(file_monitor_file_path, 'r') as f:
content = f.read()
return content
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
def read_news():
# read the news file on demand
return read_file(news_file_path)
async def watch_file():
if not os.path.exists(file_monitor_file_path):
return None
else:
last_modified_time = os.path.getmtime(file_monitor_file_path)
while True:
current_modified_time = os.path.getmtime(file_monitor_file_path)
if current_modified_time != last_modified_time:
# File has been modified
content = read_file(file_monitor_file_path)
last_modified_time = current_modified_time
# Cleanup the content
content = content.replace('\n', ' ').replace('\r', '').strip()
if content:
return content
await asyncio.sleep(1) # Check every

View File

@@ -210,22 +210,22 @@ def saveHSJack(nodeID, highScore):
# Save the game state to pickle
highScore = {'nodeID': nodeID, 'highScore': highScore}
try:
with open('blackjack_hs.pkl', 'wb') as file:
with open('data/blackjack_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new blackjack_hs.pkl file")
with open('blackjack_hs.pkl', 'wb') as file:
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('blackjack_hs.pkl', 'rb') as file:
with open('data/blackjack_hs.pkl', 'rb') as file:
highScore = pickle.load(file)
return highScore
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new blackjack_hs.pkl file")
logger.debug("System: BlackJack: Creating new data/blackjack_hs.pkl file")
highScore = {'nodeID': 0, 'highScore': 0}
with open('blackjack_hs.pkl', 'wb') as file:
with open('data/blackjack_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
return 0

View File

@@ -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 = "💊You found " + str(qty) + " of " + str(my_drugs[found])
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,8 +232,9 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1):
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(price_list[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(cost) + " "
buy_amount = value
if buy_amount == 'm':
@@ -286,15 +316,17 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
" The going price is: $" + str(price_list[drug_choice])
" The going price is: $" + str("{:,}".format(cost))
# check if the user has enough of the drug to sell
if sell_amount <= amount[drug_choice]:
amount[drug_choice] -= sell_amount
cash += sell_amount * price_list[drug_choice]
inventory -= sell_amount
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name + ' for $' +\
str(sell_amount * price_list[drug_choice]) + '. Total cash: $' + "{:,}".format(cash)
profit = sell_amount * price_list[drug_choice]
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name +\
' for $' + "{:,}".format(profit) + '. Total cash: $' + "{:,}".format(cash)
else:
msg = "You don't have that much"
return msg
@@ -317,7 +349,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
@@ -332,7 +363,6 @@ def get_location_table(nodeID, choice=0):
loc_table_string += ' Where do you want to 🛫?#'
return loc_table_string
def endGameDw(nodeID):
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
msg = ''
@@ -363,9 +393,9 @@ def endGameDw(nodeID):
# 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 +405,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 +412,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)):
@@ -429,10 +461,10 @@ def render_game_screen(userID, day_play, total_day, loc_choice, event_number, pr
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 +475,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 +494,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
@@ -570,9 +604,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,9 +617,9 @@ 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:

View File

@@ -102,12 +102,12 @@ def getScorecardGolf(scorecard):
def getHighScoreGolf(nodeID, strokes, par):
# check if player is in high score list
try:
with open('golfsim_hs.pkl', 'rb') as f:
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('golfsim_hs.pkl', 'wb') as f:
with open('data/golfsim_hs.pkl', 'wb') as f:
pickle.dump(golfHighScore, f)
if strokes < golfHighScore[0]['strokes']:
@@ -115,7 +115,7 @@ def getHighScoreGolf(nodeID, strokes, par):
golfHighScore[0]['nodeID'] = nodeID
golfHighScore[0]['strokes'] = strokes
golfHighScore[0]['par'] = par
with open('golfsim_hs.pkl', 'wb') as f:
with open('data/golfsim_hs.pkl', 'wb') as f:
pickle.dump(golfHighScore, f)
return golfHighScore
@@ -275,7 +275,7 @@ def playGolf(nodeID, message, finishedHole=False):
# 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 += "Didnt get your club 🥪♣️🪩 choice"
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:
@@ -323,6 +323,7 @@ def playGolf(nodeID, message, finishedHole=False):
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
msg += "\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:
@@ -374,7 +375,6 @@ def playGolf(nodeID, message, finishedHole=False):
# Scorecard reset
hole_to_par = 0
total_to_par = 0
hole_strokes = 0
hole_shots = 0
@@ -395,7 +395,7 @@ def playGolf(nodeID, message, finishedHole=False):
#HighScore Display
highscore = getHighScoreGolf(nodeID, total_strokes, total_to_par)
if highscore != 0:
msg += "\n🏆New Club Record🏆"
msg += " 🏆New Club Record🏆"
# pop player from tracker
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:

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

@@ -0,0 +1,126 @@
# 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
from modules.log import *
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': '🍍',
'mango': '🥭', 'apple': '🍎', 'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
'eggplant': '🍆', 'potato': '🥔', 'carrot': '🥕', 'corn': '🌽', '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': '🏪',
'school': '🏫', 'department store': '🏬', 'factory': '🏭', 'japanese castle': '🏯', 'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '', 'mosque': '🕌',
'hindu temple': '🛕', 'synagogue': '🕍', 'shinto shrine': '⛩️', 'kaaba': '🕋', 'fountain': '', 'tent': '', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'barber pole': '💈',
'robot': '🤖', 'alien': '👽', 'ghost': '👻', 'skull': '💀', 'pumpkin': '🎃', 'clown': '🤡', 'wizard': '🧙', 'elf': '🧝', 'fairy': '🧚', 'mermaid': '🧜', 'vampire': '🧛',
'zombie': '🧟', 'genie': '🧞', 'superhero': '🦸', 'supervillain': '🦹', 'mage': '🧙', 'knight': '🛡️', 'ninja': '🥷', 'pirate': '🏴‍☠️', 'angel': '👼', 'devil': '😈', 'dragon': '🐉',
'unicorn': '🦄', 'phoenix': '🦅', 'griffin': '🦅', 'centaur': '🐎', 'minotaur': '🐂', 'cyclops': '👁️', 'medusa': '🐍', 'sphinx': '🦁', 'kraken': '🦑', 'yeti': '❄️', 'sasquatch': '🦧',
'loch ness monster': '🦕', 'chupacabra': '🐐', 'banshee': '👻', 'golem': '🗿', 'djinn': '🧞', 'basilisk': '🐍', 'hydra': '🐉', 'cerberus': '🐶', 'chimera': '🐐', 'manticore': '🦁', 'wyvern': '🐉',
'pegasus': '🦄', 'hippogriff': '🦅', 'kelpie': '🐎', 'selkie': '🦭', 'kitsune': '🦊', 'tanuki': '🦝', 'tengu': '🦅', 'oni': '👹', 'yokai': '👻', 'kappa': '🐢', 'yurei': '👻',
'kami': '👼', 'shinigami': '💀', 'bakemono': '👹', 'tsukumogami': '🧸', 'noppera-bo': '👤', 'rokurokubi': '🧛', 'yuki-onna': '❄️', 'jorogumo': '🕷️', 'nue': '🐍', 'ubume': '👼',
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'rocket': '🚀', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
'comet': '☄️', 'constellation': '🌠', 'lightning': '', 'magnet': '🧲', 'battery': '🔋', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
'cd': '💿', 'dvd': '📀', 'smartphone': '📱', 'tablet': '📲', 'watch': '', 'camera': '📷', 'video camera': '📹', 'projector': '📽️', 'radio': '📻', 'television': '📺',
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'flashlight': '🔦', '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': '🐫',
'llama': '🦙', 'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
'unicorn': '🦄', 'zebra': '🦓', 'cow': '🐄', 'pig': '🐖', 'boar': '🐗', 'mouse': '🐁', 'rat': '🐀', 'hamster': '🐹', 'rabbit': '🐇', 'chipmunk': '🐿️', 'beaver': '🦫', 'hedgehog': '🦔',
'bat': '🦇', 'bear': '🐻', 'koala': '🐨', 'panda': '🐼', 'sloth': '🦥', 'otter': '🦦', 'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'turkey': '🦃', 'chicken': '🐔', 'rooster': '🐓',
'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'snake': '🐍', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖',
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨‍🏫', 'referee': '🧑‍⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
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):
dadjoke = Dadjoke()
if dad_jokes_emojiJokes:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke
return renderedLaugh

View File

@@ -22,7 +22,7 @@ lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lem
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):
@@ -41,12 +41,12 @@ def getHighScoreLemon():
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")
# 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
@@ -55,10 +55,11 @@ def start_lemonade(nodeID, message, celsius=False):
potential = 0
unit = 0.0
price = 0.0
total_sales = 0
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:
@@ -87,6 +88,7 @@ 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
@@ -115,7 +117,7 @@ def start_lemonade(nodeID, message, celsius=False):
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
# Check for end of game
if "end" in message.lower():
if message.lower().startswith("e"):
endGame(nodeID)
return "Goodbye!👋"
@@ -169,6 +171,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)
@@ -231,13 +234,12 @@ 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)
# Start the main loop
if (weeks.current <= weeks.total):
@@ -324,7 +326,7 @@ def start_lemonade(nodeID, message, celsius=False):
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()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return buffer
if "cups" in last_cmd:
@@ -351,7 +353,7 @@ def start_lemonade(nodeID, message, celsius=False):
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "lemons"
saveValues()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
return msg
@@ -381,7 +383,7 @@ def start_lemonade(nodeID, message, celsius=False):
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sugar"
saveValues()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
return msg
@@ -413,7 +415,7 @@ def start_lemonade(nodeID, message, celsius=False):
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:
@@ -438,11 +440,12 @@ def start_lemonade(nodeID, message, celsius=False):
return "The price must be greater than zero."
except Exception as e:
price = 0.00
last_cmd = "price"
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:
@@ -491,13 +494,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
total = 0
msg += "\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
@@ -535,13 +541,14 @@ def start_lemonade(nodeID, message, celsius=False):
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 " + \
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
msg += "You've sold " + str(total) + " total 🥤🍋"
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
# check for high score
high_score = getHighScoreLemon()
@@ -550,7 +557,7 @@ def start_lemonade(nodeID, message, celsius=False):
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)
@@ -560,10 +567,11 @@ def start_lemonade(nodeID, message, celsius=False):
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "new"
lemonadeTracker[i]['time'] = time.time()
weeks.current = weeks.current + 1
msg += f"Play another week🥤? 'end' to end game"
msg += f"Play another week🥤? or (E)nd Game"
saveValues()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg

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

View File

@@ -195,18 +195,19 @@ def compareCodeMMind(secret_code, user_guess):
# check for perfect pins and right color wrong position
temp_code = []
temp_guess = []
for i in range(len(user_guess)): #check for perfect pins
# 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])
for i in range(len(temp_guess)): #check for right color wrong position
for j in range(len(temp_code)):
if temp_guess[i] == temp_code[j]:
wrong_position += 1
temp_code[j] = "0"
break
# 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"Correct{getEmojiMMind(user_guess)}\n"
@@ -232,7 +233,7 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
if turn_count <= 10:
user_guess = getGuessMMind(diff, message)
if user_guess == "XXXX":
msg += "Invalid guess. Please enter 4 valid colors."
msg += f"⛔️Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
return msg
check_guess = compareCodeMMind(secret_code, user_guess)
@@ -249,7 +250,7 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
if high_score != 0:
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert?"
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
@@ -296,8 +297,6 @@ def start_mMind(nodeID, message):
if mindTracker[i]['nodeID'] == nodeID:
last_cmd = mindTracker[i]['cmd']
logger.debug("System: MasterMind: last_cmd: " + str(last_cmd))
if last_cmd == "new":
if message.lower().startswith("n") or message.lower().startswith("h") or message.lower().startswith("x"):
diff = message.lower()[0]

View File

@@ -165,7 +165,7 @@ class PlayerVP:
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
def score_hand(self, resetHand = True):
@@ -276,23 +276,23 @@ def saveHSVp(nodeID, highScore):
# Save the game high_score to pickle
highScore = {'nodeID': nodeID, 'highScore': highScore}
try:
with open('videopoker_hs.pkl', 'wb') as file:
with open('data/videopoker_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
except FileNotFoundError:
logger.debug("System: BlackJack: Creating new videopoker_hs.pkl file")
with open('videopoker_hs.pkl', 'wb') as file:
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('videopoker_hs.pkl', 'rb') as file:
with open('data/videopoker_hs.pkl', 'rb') as file:
highScore = pickle.load(file)
return highScore
except FileNotFoundError:
logger.debug("System: VideoPoker: Creating new videopoker_hs.pkl file")
logger.debug("System: VideoPoker: Creating new data/videopoker_hs.pkl file")
highScore = {'nodeID': 0, 'highScore': 0}
with open('videopoker_hs.pkl', 'wb') as file:
with open('data/videopoker_hs.pkl', 'wb') as file:
pickle.dump(highScore, file)
return 0
@@ -326,15 +326,15 @@ def playVideoPoker(nodeID, message):
try:
bet = int(message)
except ValueError:
msg += "Please enter a valid bet amount. 1 to 5 coins."
msg += f"Please enter a valid bet, 1 to 5 coins. you have {player.bankroll} coins."
# Check if bet is valid
if bet > player.bankroll:
msg += "You can only bet the money you have. No strip poker here..."
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."
msg += "You must bet at least 1 coin.🪙"
elif bet > 5:
msg += "You can only bet up to 5 coins."
msg += "The 🎰 coin slot only fits 5 coins max."
# if msg contains an error, return it
if msg is not None and msg != '':
@@ -433,7 +433,7 @@ def playVideoPoker(nodeID, message):
# 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

View File

@@ -4,18 +4,29 @@
# K7MHI Kelly Keeton 2024
from modules.log import *
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
# Ollama Client
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
from ollama import Client as OllamaClient
from googlesearch import search # pip install googlesearch-python
# This is my attempt at a simple RAG implementation it will require some setup
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
# This is lighter weight and can be used in a standalone environment, needs chromadb
# "chat with a file" is the use concept here, the file is the RAG data
ragDEV = False
if ragDEV:
import os
import ollama # pip install ollama
import chromadb # pip install chromadb
# LLM System Variables
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
ollamaClient = OllamaClient(host=ollamaHostName)
llmEnableHistory = True # enable last message history for the LLM model
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
antiFloodLLM = []
llmChat_history = []
llmChat_history = {}
trap_list_llm = ("ask:", "askai")
meshBotAI = """
@@ -24,49 +35,106 @@ meshBotAI = """
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
You must respond in plain text standard ASCII characters, or emojis.
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
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}
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}
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}
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
def llm_readTextFiles():
# read .txt files in ../data/rag
try:
text = []
directory = "../data/rag"
for filename in os.listdir(directory):
if filename.endswith(".txt"):
filepath = os.path.join(directory, filename)
with open(filepath, 'r') as f:
text.append(f.read())
return text
except Exception as e:
logger.debug(f"System: LLM readTextFiles: {e}")
return False
def store_text_embedding(text):
try:
# store each document in a vector embedding database
for i, d in enumerate(text):
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
embedding = response["embedding"]
collection.add(
ids=[str(i)],
embeddings=[embedding],
documents=[d]
)
except Exception as e:
logger.debug(f"System: Embedding failed: {e}")
return False
## INITALIZATION of RAG
if ragDEV:
try:
chromaHostname = "localhost:8000"
# connect to the chromaDB
chromaHost = chromaHostname.split(":")[0]
chromaPort = chromaHostname.split(":")[1]
if chromaHost == "localhost" and chromaPort == "8000":
# create a client using local python Client
chromaClient = chromadb.Client()
else:
# create a client using the remote python Client
# this isnt tested yet please test and report back
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
clearCollection = False
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
chromaClient.delete_collection("meshBotAI")
# create a new collection
collection = chromaClient.create_collection("meshBotAI")
logger.debug(f"System: LLM: Cataloging RAG data")
store_text_embedding(llm_readTextFiles())
except Exception as e:
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
def query_collection(prompt):
# generate an embedding for the prompt and retrieve the most relevant doc
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
data = results['documents'][0][0]
return data
def llm_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
@@ -90,7 +158,6 @@ def llm_query(input, nodeID=0, location_name=None):
# 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:
@@ -103,9 +170,10 @@ def llm_query(input, nodeID=0, location_name=None):
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
history = llmChat_history.get(nodeID, ["", ""])
if googleResults:
logger.debug(f"System: LLM Query: {input} From:{nodeID} with context from google")
logger.debug(f"System: Google-Enhanced LLM Query: {input} From:{nodeID}")
else:
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
@@ -114,28 +182,40 @@ def llm_query(input, nodeID=0, location_name=None):
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \
"history": llmChat_history, "context": googleResults, "location_name": location_name})
# RAG context inclusion testing
ragContext = False
if ragDEV:
ragContext = query_collection(input)
if ragContext:
ragContextGooogle = ragContext + '\n'.join(googleResults)
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
# Query the model with RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
else:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# Query the model without RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
if isinstance(result, dict):
result = result.get("response")
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
logger.warning(f"System: LLM failure: {e}")
return "I am having trouble processing your request, please try again later."
# cleanup for message output
response = result.strip().replace('\n', ' ')
# 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))
# done with the query, remove the user from the anti flood list
antiFloodLLM.remove(nodeID)
if llmEnableHistory:
llmChat_history[nodeID] = [input, response]
return response
# import subprocess

View File

@@ -9,9 +9,9 @@ import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
def where_am_i(lat=0, lon=0, short=False):
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
grid = mh.to_maiden(float(lat), float(lon))
@@ -31,24 +31,130 @@ def where_am_i(lat=0, lon=0, short=False):
whereIam = f"City: {address.get('city', '')}. State: {address.get('state', '')}. County: {address.get('county', '')}. Country: {address.get('country', '')}."
return whereIam
if zip:
# return a string with zip code only
location = geolocator.reverse(str(lat) + ", " + str(lon))
whereIam = location.raw['address'].get('postcode', '')
return whereIam
if float(lat) == latitudeValue and float(lon) == longitudeValue:
# redacted address when no GPS and using default location
location = geolocator.reverse(lat + ", " + lon)
location = geolocator.reverse(str(lat) + ", " + str(lon))
address = location.raw['address']
address_components = ['city', 'state', 'postcode', 'county', 'country']
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
whereIam += " Grid: " + grid
address_components = {
'city': 'City',
'state': 'State',
'postcode': 'Zip',
'county': 'County',
'country': 'Country'
}
whereIam += ', '.join([f"{label}: {address.get(component, '')}" for component, label in address_components.items() if component in address])
else:
location = geolocator.reverse(lat + ", " + lon)
address = location.raw['address']
address_components = ['house_number', 'road', 'city', 'state', 'postcode', 'county', 'country']
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
whereIam += " Grid: " + grid
address_components = {
'house_number': 'Number',
'road': 'Road',
'city': 'City',
'state': 'State',
'postcode': 'Zip',
'county': 'County',
'country': 'Country'
}
whereIam += ', '.join([f"{label}: {address.get(component, '')}" for component, label in address_components.items() if component in address])
whereIam += f", Grid: " + grid
return whereIam
except Exception as e:
logger.debug("Location:Error fetching location data with whereami, likely network error")
return ERROR_FETCHING_DATA
def getRepeaterBook(lat=0, lon=0):
grid = mh.to_maiden(float(lat), float(lon))
data = []
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
try:
msg = ''
response = requests.get(repeater_url)
soup = bs.BeautifulSoup(response.text, 'html.parser')
table = soup.find('table', attrs={'class': 'w3-table w3-striped w3-responsive w3-mobile w3-auto sortable'})
if table is not None:
cells = table.find_all('td')
data = []
for i in range(0, len(cells), 11):
if i + 10 < len(cells): #avoid IndexError
repeater = {
'frequency': cells[i].text.strip() if i < len(cells) else 'N/A',
'offset': cells[i + 1].text.strip() if i + 1 < len(cells) else 'N/A',
'tone': cells[i + 2].text.strip() if i + 2 < len(cells) else 'N/A',
'call_sign': cells[i + 3].text.strip() if i + 3 < len(cells) else 'N/A',
'location': cells[i + 4].text.strip() if i + 4 < len(cells) else 'N/A',
'state': cells[i + 5].text.strip() if i + 5 < len(cells) else 'N/A',
'use': cells[i + 6].text.strip() if i + 6 < len(cells) else 'N/A',
'mode': cells[i + 7].text.strip() if i + 7 < len(cells) else 'N/A',
'distance': cells[i + 8].text.strip() if i + 8 < len(cells) else 'N/A',
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
}
data.append(repeater)
else:
msg = "bug?Not enough columns"
else:
msg = "bug?Table not found"
except Exception as e:
msg = "No repeaters found 😔"
# Limit the output to the first 4 repeaters
for repeater in data[:4]:
tmpTone = repeater['tone'].replace(" /", "")
msg += f"{repeater['call_sign']}📶{repeater['frequency']}{repeater['offset']},{tmpTone}.{repeater['mode']}"
if repeater != data[:4][-1]: msg += '\n'
return msg
def getArtSciRepeaters(lat=0, lon=0):
# UK api_url = "https://api-beta.rsgb.online/all/systems"
#grid = mh.to_maiden(float(lat), float(lon))
repeaters = []
zipCode = where_am_i(lat, lon, zip=True)
if zipCode == NO_DATA_NOGPS or zipCode == ERROR_FETCHING_DATA:
return zipCode
if zipCode.isnumeric():
try:
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
response = requests.get(artsci_url)
soup = bs.BeautifulSoup(response.text, 'html.parser')
# results needed xpath is /html/body/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table
table = soup.find_all('table')[1]
rows = table.find_all('tr')
for row in rows:
cols = row.find_all('td')
cols = [ele.text.strip() for ele in cols]
# if no elements have the word 'located' then append
if not any('located' in ele for ele in cols):
if not any('Location' in ele for ele in cols):
repeaters.append([ele for ele in cols if ele])
except Exception as e:
logger.error(f"Error fetching data from {artsci_url}: {e}")
if repeaters != []:
msg = f"Found:{len(repeaters)} in {zipCode}\n"
for repeater in repeaters:
# format is ['City', 'Frequency', 'Offset', 'PL', 'Call', 'Notes']
# there might be missing elements or only one element
if len(repeater) == 2:
msg += f"Freq:{repeater[1]}"
elif len(repeater) == 3:
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}"
elif len(repeater) == 4:
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID: {repeater[3]}"
elif len(repeater) == 5:
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID:{repeater[3]}"
elif len(repeater) == 6:
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID:{repeater[3]}. {repeater[5]}"
if repeater != repeaters[-1]:
msg += "\n"
else:
msg = f"no results.. sorry"
return msg
def get_tide(lat=0, lon=0):
station_id = ""
@@ -74,40 +180,44 @@ def get_tide(lat=0, lon=0):
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
if zuluTime:
station_url += "&clock=24hour"
station_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&time_zone=lst_ldt&datum=MLLW&product=predictions&interval=hilo&format=json&station=" + station_id
if use_metric:
station_url += "&units=metric"
else:
station_url += "&units=english"
try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok:
logger.error("Location:Error fetching station data from NOAA")
tide_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if tide_data.ok:
tide_json = tide_data.json()
else:
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA
# extract table class="table table-condensed"
soup = bs.BeautifulSoup(station_data.text, 'html.parser')
table = soup.find('table', class_='table table-condensed')
# extract rows
rows = table.find_all('tr')
# extract data from rows
tide_data = []
for row in rows:
row_text = ""
cols = row.find_all('td')
for col in cols:
row_text += col.text + " "
tide_data.append(row_text)
# format tide data into a string
tide_string = ""
for data in tide_data:
tide_string += data + "\n"
# trim off last newline
tide_string = tide_string[:-1]
return tide_string
except (requests.exceptions.RequestException, json.JSONDecodeError):
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
tide_data = tide_json['predictions']
# format tide data into a table string for mesh
# get the date out of the first t value
tide_date = tide_data[0]['t'].split(" ")[0]
tide_table = "Tide Data for " + tide_date + "\n"
for tide in tide_data:
tide_time = tide['t'].split(" ")[1]
if not zuluTime:
# convert to 12 hour clock
if int(tide_time.split(":")[0]) > 12:
tide_time = str(int(tide_time.split(":")[0]) - 12) + ":" + tide_time.split(":")[1] + " PM"
else:
tide_time = tide_time + " AM"
tide_table += tide['type'] + " " + tide_time + ", " + tide['v'] + "\n"
# remove last newline
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
@@ -215,11 +325,15 @@ def abbreviate_weather(row):
return line
def getWeatherAlerts(lat=0, lon=0):
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
if float(lat) == 0 and float(lon) == 0:
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -259,6 +373,25 @@ def getWeatherAlerts(lat=0, lon=0):
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
return data
wxAlertCache = ""
def alertBrodcast():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
# check if any reason to discard the alerts
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
return False
elif currentAlert == NO_ALERTS:
wxAlertCache = ""
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""

View File

@@ -1,8 +1,6 @@
# 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 logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from modules.settings import *
@@ -33,6 +31,13 @@ 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_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
def format(self, record):
message = super().format(record)
return self.ansi_escape.sub('', message)
# Create logger
logger = logging.getLogger("MeshBot System Logger")
@@ -54,19 +59,19 @@ stdout_handler = logging.StreamHandler()
stdout_handler.setLevel(logging.DEBUG)
# 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('logs/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)
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler_sys.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('logs/messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)
msgLogger.addHandler(file_handler)

View File

@@ -5,7 +5,7 @@ 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'
MOTD = 'Thanks for using MeshBOT! Have a good day!'
NO_ALERTS = "No weather alerts found."
@@ -19,11 +19,11 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
retry_int1 = False
retry_int2 = False
scheduler_enabled = False # enable the scheduler currently config via code only
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
@@ -46,15 +46,15 @@ if config.sections() == []:
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['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['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['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
if 'repeater' not in config:
@@ -73,6 +73,14 @@ if 'messagingSettings' not in config:
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'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
@@ -96,8 +104,9 @@ try:
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
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)
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
store_forward_enabled = config['general'].getboolean('StoreForward', True)
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
@@ -109,10 +118,12 @@ try:
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
whoami_enabled = config['general'].getboolean('whoami', True)
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
# sentry
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
@@ -130,17 +141,31 @@ try:
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if wxAlertBroadcastChannel:
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
# 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(',')
# repeater
repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
# scheduler
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
@@ -149,8 +174,16 @@ try:
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
dopewars_enabled = config['games'].getboolean('dopeWars', True)
lemonade_enabled = config['games'].getboolean('lemonade', True)
blackjack_enabled = config['games'].getboolean('blackjack', True)
@@ -162,6 +195,8 @@ try:
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
except KeyError as e:
print(f"System: Error reading config file: {e}")

File diff suppressed because it is too large Load Diff

1
news.txt Normal file
View File

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

View File

@@ -13,7 +13,5 @@ numpy
geopy
schedule
wikipedia
langchain
langchain-ollama
ollama
googlesearch-python