Compare commits

...

441 Commits
RC8 ... v1.2

Author SHA1 Message Date
SpudGunMan
e7daae9250 Update mesh_bot.py
Therac-25
2024-09-21 19:02:01 -07:00
SpudGunMan
91a188f3fd Update config.template 2024-09-21 18:35:57 -07:00
SpudGunMan
49a6d48101 Update config.template 2024-09-21 18:35:04 -07:00
SpudGunMan
a4b5ed3597 Update mesh_bot.py 2024-09-21 17:33:59 -07:00
SpudGunMan
2efbfeef8c Update blackjack.py 2024-09-21 16:59:04 -07:00
SpudGunMan
1a9093bbdf Update blackjack.py 2024-09-21 16:47:04 -07:00
SpudGunMan
9b3d3c6bdc enhance 2024-09-21 16:44:52 -07:00
SpudGunMan
620a6f4795 fix loss payout
the payout has been taken in the betPlaced so its gone already no reason to recalculate (which subtracts it from bank)
2024-09-21 16:33:31 -07:00
SpudGunMan
1615fa9e51 Update videopoker.py 2024-09-21 16:21:57 -07:00
SpudGunMan
205906ebb7 Update mesh_bot.py 2024-09-21 16:19:32 -07:00
SpudGunMan
0d8016651c bet correction for video poker
the winnings multiplier considers the base bet, so there is no reason to add it on after the calculations its a free bet every time loss for the casino we need to account for these coins
2024-09-21 16:16:13 -07:00
SpudGunMan
ffdc776413 Update videopoker.py 2024-09-21 15:44:49 -07:00
SpudGunMan
9b92b3d1ae Update dopewar.py 2024-09-21 14:53:00 -07:00
SpudGunMan
3bfea2fd0e Update dopewar.py 2024-09-21 14:36:48 -07:00
SpudGunMan
97636ca575 Update dopewar.py 2024-09-21 14:36:14 -07:00
SpudGunMan
8c4c2095d7 Update dopewar.py 2024-09-21 14:25:51 -07:00
SpudGunMan
9855656e9a Update dopewar.py 2024-09-21 14:23:10 -07:00
SpudGunMan
cffdf92daf Update mesh_bot.py 2024-09-21 13:15:49 -07:00
SpudGunMan
d07ca4076e Update lemonade.py 2024-09-21 01:55:17 -07:00
SpudGunMan
47740df9a9 Update lemonade.py 2024-09-21 01:43:39 -07:00
SpudGunMan
e830d3fef9 Update lemonade.py 2024-09-21 01:43:03 -07:00
SpudGunMan
3c0a87354e Update lemonade.py 2024-09-21 01:32:32 -07:00
SpudGunMan
b5a2eb9fb9 Update lemonade.py 2024-09-21 01:21:02 -07:00
SpudGunMan
244bd02771 Update lemonade.py 2024-09-21 01:15:10 -07:00
SpudGunMan
95a978495d text💣 2024-09-21 01:04:59 -07:00
SpudGunMan
292ad8bb61 ahhh 2024-09-21 00:49:56 -07:00
SpudGunMan
8b0a2263e7 Update lemonade.py 2024-09-21 00:49:36 -07:00
SpudGunMan
bb9ffe3492 forgot to. enhance this 2024-09-21 00:44:34 -07:00
SpudGunMan
ea7ef074e1 enhance 2024-09-21 00:40:41 -07:00
SpudGunMan
897d6727b4 enhance 2024-09-21 00:30:46 -07:00
SpudGunMan
0f99728be3 Update lemonade.py 2024-09-21 00:23:12 -07:00
SpudGunMan
51903d04f5 Update lemonade.py 2024-09-21 00:03:38 -07:00
SpudGunMan
dde7dbf3a0 Update lemonade.py 2024-09-21 00:00:13 -07:00
SpudGunMan
508998efa8 Update lemonade.py 2024-09-20 23:24:34 -07:00
SpudGunMan
2691247532 Update lemonade.py 2024-09-20 23:07:02 -07:00
SpudGunMan
f3c9d8211f Update lemonade.py 2024-09-20 23:03:58 -07:00
SpudGunMan
ec970d9f08 Update lemonade.py 2024-09-20 23:00:41 -07:00
SpudGunMan
04b86ae7e4 Update lemonade.py 2024-09-20 22:56:47 -07:00
SpudGunMan
b7b31ca5c2 Update lemonade.py 2024-09-20 22:55:21 -07:00
SpudGunMan
7c76e3b3a9 Update lemonade.py 2024-09-20 22:53:45 -07:00
SpudGunMan
863f8d8630 Update lemonade.py 2024-09-20 22:49:06 -07:00
SpudGunMan
b12d16f7a7 Update dopewar.py 2024-09-20 22:36:41 -07:00
SpudGunMan
ead8474783 Update lemonade.py 2024-09-20 22:29:50 -07:00
SpudGunMan
03a1ead721 Update lemonade.py 2024-09-20 22:25:48 -07:00
SpudGunMan
234dbb60fe Update mesh_bot.py 2024-09-20 22:03:05 -07:00
SpudGunMan
43503df53c Update mesh_bot.py 2024-09-20 22:02:17 -07:00
Kelly
6c381aecb7 Merge pull request #62 from SpudGunMan/lab
Nestpebble Enhancements
2024-09-20 21:36:51 -07:00
SpudGunMan
8d1e366e10 Update system.py 2024-09-20 21:34:16 -07:00
SpudGunMan
d6a0713f52 Update settings.py 2024-09-20 21:33:11 -07:00
SpudGunMan
113bde4bab Nestpebble Ideas
@Nestpebble here is the changes I did to bring this in thank you for the ideas and enhancements!! 🥔

* whoami new command
* enhancing MOTD
* new and better icons
2024-09-20 21:30:18 -07:00
Kelly
b5354a2431 Merge pull request #61 from Nestpebble/main
Nestpebble enhancements
2024-09-20 20:26:24 -07:00
SpudGunMan
c166dd9ed8 Update mesh_bot.py 2024-09-20 19:47:06 -07:00
SpudGunMan
80b73da72c Update blackjack.py 2024-09-20 19:24:07 -07:00
SpudGunMan
98490e6444 WHITESPACE 2024-09-20 18:54:38 -07:00
SpudGunMan
56649471d0 Update blackjack.py 2024-09-20 18:48:39 -07:00
SpudGunMan
0186bdc15a Update mesh_bot.py 2024-09-20 18:43:54 -07:00
SpudGunMan
eb02014371 Update mesh_bot.py 2024-09-20 18:34:10 -07:00
SpudGunMan
993292cec8 Update blackjack.py 2024-09-20 18:30:50 -07:00
SpudGunMan
aac99eaf07 Update blackjack.py 2024-09-20 18:28:15 -07:00
SpudGunMan
e5d82df65c Update blackjack.py 2024-09-20 18:02:10 -07:00
SpudGunMan
10bbbecf7b Update videopoker.py 2024-09-20 17:54:23 -07:00
SpudGunMan
8b5f7c4bcb Update simulator.py 2024-09-20 17:40:51 -07:00
SpudGunMan
10c568fcac enhance 2024-09-20 12:48:35 -07:00
SpudGunMan
7443a2fbc4 Update blackjack.py 2024-09-20 12:29:28 -07:00
SpudGunMan
3e5681abc5 Update README.md 2024-09-20 12:11:16 -07:00
SpudGunMan
e5036c0b45 Update README.md 2024-09-20 12:07:52 -07:00
SpudGunMan
a312a81141 enhance 2024-09-20 12:04:45 -07:00
SpudGunMan
40dd55d828 enhance 2024-09-20 11:34:44 -07:00
SpudGunMan
135bb622e1 Update videopoker.py 2024-09-20 11:20:48 -07:00
SpudGunMan
ed8911be5f Update videopoker.py 2024-09-20 11:12:43 -07:00
SpudGunMan
a126d0610f Update videopoker.py 2024-09-20 11:02:37 -07:00
SpudGunMan
946301ba86 Update videopoker.py 2024-09-20 10:57:48 -07:00
SpudGunMan
604e8ec366 Update videopoker.py 2024-09-20 10:36:23 -07:00
SpudGunMan
2593dd4e86 Vpoker 2024-09-20 10:22:51 -07:00
SpudGunMan
9ee9cdd969 Update mesh_bot.py 2024-09-20 01:21:31 -07:00
SpudGunMan
ad3918f071 Update mesh_bot.py 2024-09-20 01:08:07 -07:00
SpudGunMan
3cbc1efa59 Update mesh_bot.py 2024-09-20 01:02:30 -07:00
SpudGunMan
446f0336b6 Update mesh_bot.py 2024-09-20 00:58:07 -07:00
SpudGunMan
e3c3becb63 Update mesh_bot.py 2024-09-20 00:56:01 -07:00
SpudGunMan
8564c85378 Update mesh_bot.py 2024-09-20 00:40:20 -07:00
SpudGunMan
eedca4c1d0 Update videopoker.py 2024-09-19 23:38:06 -07:00
SpudGunMan
28c46dcbfd Update mesh_bot.py 2024-09-19 23:19:42 -07:00
SpudGunMan
544c4c3e80 Update mesh_bot.py 2024-09-19 23:18:06 -07:00
SpudGunMan
8bfd1a4d11 Update mesh_bot.py 2024-09-19 23:13:35 -07:00
SpudGunMan
3fae6241a7 refactorLLMlocations 2024-09-19 23:06:13 -07:00
SpudGunMan
48ff4209c4 Update mesh_bot.py 2024-09-19 22:01:41 -07:00
SpudGunMan
1d3986c589 fix a dict list 2024-09-19 21:55:39 -07:00
SpudGunMan
52b2ec776b Update mesh_bot.py 2024-09-19 21:51:34 -07:00
SpudGunMan
9d3c6b28ef GAMEDELAY 2024-09-19 21:26:43 -07:00
SpudGunMan
c242eb3750 documentation 2024-09-19 21:22:57 -07:00
SpudGunMan
e718fea565 wopr 2024-09-19 21:19:58 -07:00
SpudGunMan
14644da9c1 Update videopoker.py 2024-09-19 20:40:53 -07:00
SpudGunMan
0c99dc52d3 Update videopoker.py 2024-09-19 20:40:35 -07:00
SpudGunMan
9cf94d2d1d Update videopoker.py 2024-09-19 20:37:04 -07:00
SpudGunMan
de560f9b8a Update videopoker.py 2024-09-19 20:30:17 -07:00
SpudGunMan
47b8b7a9b8 Update videopoker.py 2024-09-19 20:28:11 -07:00
SpudGunMan
216c468118 Update videopoker.py 2024-09-19 20:24:39 -07:00
SpudGunMan
dd6735167b Update videopoker.py 2024-09-19 20:19:09 -07:00
SpudGunMan
a400eabc0e Update videopoker.py 2024-09-19 20:13:53 -07:00
SpudGunMan
3b95f1b873 VideoPoker 2024-09-19 20:10:46 -07:00
SpudGunMan
7b1441814d Update simulator.py 2024-09-19 16:45:04 -07:00
SpudGunMan
cc39a3a173 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2024-09-19 16:01:30 -07:00
SpudGunMan
34df5cfc6a Update .gitignore 2024-09-19 16:01:29 -07:00
Kelly
37836e7842 Merge pull request #60 from SpudGunMan/games
GAMES!
2024-09-19 16:00:15 -07:00
SpudGunMan
121231f6fa Update README.md 2024-09-19 15:57:42 -07:00
SpudGunMan
71c8c98ce9 Black21Jack! 2024-09-19 15:48:10 -07:00
SpudGunMan
2c3cce4d51 Update dopewar.py 2024-09-19 11:54:05 -07:00
SpudGunMan
34ed1247e3 Update mesh_bot.py 2024-09-19 11:53:06 -07:00
SpudGunMan
e8351a1136 Update mesh_bot.py 2024-09-19 11:52:03 -07:00
SpudGunMan
4f93e3f5e2 Update lemonade.py 2024-09-19 11:24:59 -07:00
SpudGunMan
33017aa948 Update lemonade.py 2024-09-19 11:19:21 -07:00
SpudGunMan
cbfe68230c Update lemonade.py 2024-09-19 11:13:38 -07:00
SpudGunMan
775c998e4d oofWhySoPainfiull 2024-09-19 11:03:12 -07:00
SpudGunMan
46b8fc7dcd Update dopewar.py 2024-09-19 10:59:33 -07:00
SpudGunMan
d671eb14ce Update dopewar.py 2024-09-19 10:57:29 -07:00
SpudGunMan
cd7d4969ef Update dopewar.py 2024-09-19 10:54:24 -07:00
SpudGunMan
a4e7509354 Update dopewar.py 2024-09-19 10:49:23 -07:00
SpudGunMan
cd74573adc Update dopewar.py 2024-09-19 10:43:45 -07:00
SpudGunMan
86f14ad88a Update dopewar.py 2024-09-19 10:29:31 -07:00
SpudGunMan
847634e524 Update lemonade.py 2024-09-19 01:34:11 -07:00
SpudGunMan
f162019f77 Update lemonade.py 2024-09-19 01:28:53 -07:00
SpudGunMan
b814632f74 Update dopewar.py 2024-09-19 01:26:13 -07:00
SpudGunMan
741ced6289 Update dopewar.py 2024-09-19 01:20:47 -07:00
SpudGunMan
d939a69d16 Update dopewar.py 2024-09-19 01:18:45 -07:00
SpudGunMan
08416cb4e1 Update dopewar.py 2024-09-19 01:18:09 -07:00
SpudGunMan
a30a607f4f Update dopewar.py 2024-09-19 01:17:10 -07:00
SpudGunMan
5caae28b55 Update dopewar.py 2024-09-19 01:16:23 -07:00
SpudGunMan
c85b9d9ee1 Update dopewar.py 2024-09-19 01:10:06 -07:00
SpudGunMan
3d8c8520de Update lemonade.py 2024-09-19 00:58:18 -07:00
SpudGunMan
9e8aec16e5 Update dopewar.py 2024-09-19 00:56:18 -07:00
SpudGunMan
91ea47a6f4 Update simulator.py 2024-09-18 16:36:41 -07:00
SpudGunMan
c0d108ee77 enhance 2024-09-18 16:35:29 -07:00
SpudGunMan
94479f47d9 fixHighScore 2024-09-18 16:08:43 -07:00
SpudGunMan
f6fd017871 Update simulator.py 2024-09-18 15:55:18 -07:00
SpudGunMan
15de706fba Update simulator.py 2024-09-18 15:54:39 -07:00
SpudGunMan
f7745ac556 Update simulator.py 2024-09-18 15:51:48 -07:00
SpudGunMan
ad32ed5a80 Create simulator.py
simulator for prototyping code bits
2024-09-18 15:45:38 -07:00
SpudGunMan
18e1522374 fixTypo 2024-09-18 15:03:40 -07:00
SpudGunMan
bd01da15cc gamesTables 2024-09-18 15:02:00 -07:00
SpudGunMan
1b4ca71e38 Update bbsdb_admin.py 2024-09-18 14:59:40 -07:00
SpudGunMan
68fabef2be Update mesh_bot.py 2024-09-18 14:54:37 -07:00
SpudGunMan
c07f756d67 enhanceDM logic 2024-09-18 01:21:26 -07:00
SpudGunMan
bf52369d2d endgame cleanup 2024-09-18 01:14:57 -07:00
SpudGunMan
e49e63f39a Update dopewar.py 2024-09-18 01:09:36 -07:00
SpudGunMan
f651f2e577 fix stale players 2024-09-18 01:05:28 -07:00
SpudGunMan
f424da410b fixes 2024-09-17 03:13:17 -07:00
SpudGunMan
ada809066c highscore 2024-09-17 02:54:45 -07:00
SpudGunMan
e7ec8518e7 Update lemonade.py 2024-09-17 02:34:50 -07:00
SpudGunMan
1ea6a4d987 Update lemonade.py 2024-09-17 02:32:12 -07:00
SpudGunMan
f111aad6b7 Games
A late night double feature of dopewars and lemonade stand.
2024-09-17 02:21:31 -07:00
Nestpebble
88282db0e4 put everything where it should be... 2024-09-16 12:08:33 +01:00
Nestpebble
55e09938bb sdrfsdf 2024-09-16 11:55:44 +01:00
Nestpebble
add6c73af7 asdasd 2024-09-16 11:54:44 +01:00
Nestpebble
62a932b9ed sdfsdf 2024-09-16 11:51:16 +01:00
Nestpebble
037024395d asdasd 2024-09-16 11:49:07 +01:00
Nestpebble
e2105e1fa3 zdzsdf 2024-09-16 11:46:21 +01:00
Nestpebble
be67b33f99 dsasd 2024-09-16 11:45:20 +01:00
Nestpebble
7224da9c7a dfsdf 2024-09-16 11:41:54 +01:00
Nestpebble
fd68007c41 sedfsedf 2024-09-16 11:31:46 +01:00
Nestpebble
e77c7935de Update mesh_bot.py 2024-09-16 11:21:58 +01:00
Nestpebble
cf0072fce4 asdasd 2024-09-16 11:14:37 +01:00
Nestpebble
6f8a8b4264 sdfsdf 2024-09-16 11:13:13 +01:00
Nestpebble
746db62fdb sdfsdf 2024-09-16 11:12:32 +01:00
Nestpebble
fa4e254ba4 sfsdf 2024-09-16 11:09:06 +01:00
Nestpebble
0a2e385f41 see if this works 2024-09-16 10:43:58 +01:00
Nestpebble
2d6e939306 sdfsdf 2024-09-16 10:27:56 +01:00
Nestpebble
b5d7107760 sdfsdf 2024-09-16 10:26:14 +01:00
Nestpebble
575dd08b74 strings 2024-09-16 10:20:25 +01:00
Nestpebble
b043541dca traplist updated 2024-09-16 10:17:09 +01:00
Nestpebble
fa5fd85b5d Update mesh_bot.py 2024-09-16 10:12:17 +01:00
Nestpebble
be68cece47 Update mesh_bot.py 2024-09-16 10:11:24 +01:00
Nestpebble
50245e618b Update mesh_bot.py 2024-09-16 09:56:48 +01:00
Nestpebble
e0d85a65f9 Update mesh_bot.py 2024-09-16 09:56:03 +01:00
Nestpebble
a414a994fb Update mesh_bot.py 2024-09-16 09:42:57 +01:00
Nestpebble
c0e7f4c0fa Update mesh_bot.py 2024-09-16 09:04:38 +01:00
Nestpebble
c03375c7f0 Update mesh_bot.py 2024-09-16 09:04:28 +01:00
Nestpebble
453cfbb306 Update mesh_bot.py 2024-09-16 02:30:22 +01:00
Nestpebble
deb5ee8374 Update mesh_bot.py 2024-09-16 02:29:34 +01:00
Nestpebble
5d2f7dbe8a Update mesh_bot.py 2024-09-16 02:13:10 +01:00
Nestpebble
3831998add Update mesh_bot.py 2024-09-16 02:12:13 +01:00
Nestpebble
e85982e17c Update mesh_bot.py 2024-09-16 02:00:04 +01:00
Nestpebble
46c17c8470 Update mesh_bot.py 2024-09-16 01:54:38 +01:00
Nestpebble
5fd326f2e8 Update mesh_bot.py add nodeID check to MOTD change 2024-09-16 01:41:43 +01:00
Nestpebble
ad5e086eb5 Update mesh_bot.py handle_testing 2024-09-16 01:22:17 +01:00
Nestpebble
ef9231f51f Update settings.py 2024-09-16 01:20:11 +01:00
Nestpebble
bf34661e42 Update settings.py 2024-09-16 01:19:06 +01:00
Nestpebble
286db4fbea add messagingSettings 2024-09-16 00:44:46 +01:00
Nestpebble
4f4dbfbc6f add messagingSettings 2024-09-16 00:44:17 +01:00
Nestpebble
1046daaf16 remove responseDelay from here, moved to settings.ini 2024-09-16 00:43:50 +01:00
Nestpebble
b7df9d05a7 Add time.sleep(splitDelay) between split messages 2024-09-16 00:42:59 +01:00
Nestpebble
394c3ef4f6 update ping messages 2024-09-16 00:16:42 +01:00
Nestpebble
7a071c33cd Merge branch 'SpudGunMan:main' into main 2024-09-13 23:28:47 +01:00
SpudGunMan
71733de05f enhance DM/Channel logic for llm
fix logic for DM or Channel Sending
2024-09-10 12:59:00 -07:00
SpudGunMan
d5e48a3e36 Update README.md 2024-09-10 00:56:16 -07:00
SpudGunMan
e36755a21d Update llm.py
fix typo
2024-09-10 00:52:20 -07:00
SpudGunMan
9620164884 Update locationdata.py
enhance for LLM
2024-09-10 00:46:43 -07:00
SpudGunMan
711844cc83 enhanceMessages
fixes a few things I didnt wrap up and also enhances this suggestion https://github.com/SpudGunMan/meshing-around/issues/59
2024-09-10 00:46:32 -07:00
SpudGunMan
6eb82b26a7 Update mesh_bot.py 2024-09-09 23:27:12 -07:00
SpudGunMan
b12cf6219a responseDelay 2024-09-06 09:31:30 -07:00
SpudGunMan
7e46305277 responseDelay 2024-09-06 09:30:07 -07:00
Nestpebble
14aa127f31 Update mesh_bot.py 2024-09-06 13:33:30 +01:00
Nestpebble
6e7e89c2d0 Update pong_bot.py 2024-09-06 13:31:46 +01:00
SpudGunMan
819c37bdec networkErrorFix 2024-09-05 23:01:46 -07:00
SpudGunMan
c80690d66e Update README.md 2024-09-05 09:38:56 -07:00
SpudGunMan
084f879537 Update settings.py 2024-09-05 09:09:38 -07:00
SpudGunMan
7afd6bbbe9 Update README.md 2024-09-05 00:54:32 -07:00
SpudGunMan
46641e8a86 Update README.md 2024-09-05 00:51:01 -07:00
SpudGunMan
010b386ce1 Update README.md 2024-09-05 00:43:02 -07:00
SpudGunMan
a73b320715 Update llm.py 2024-09-05 00:22:33 -07:00
SpudGunMan
5f822a6230 Update mesh_bot.py 2024-09-04 22:27:15 -07:00
SpudGunMan
024fac90cd Update install.sh 2024-09-04 19:50:49 -07:00
SpudGunMan
133bc36cca Update install.sh 2024-09-04 19:22:21 -07:00
SpudGunMan
51a7ff2820 checkPip 2024-09-04 19:17:37 -07:00
SpudGunMan
bc96f8df49 installOllama 2024-09-04 18:42:04 -07:00
Kelly
979f197476 Merge pull request #57 from SpudGunMan/llmLocationAware
LLM location aware enhancement
2024-09-04 17:18:03 -07:00
SpudGunMan
1677b69363 comments# 2024-09-04 15:10:38 -07:00
SpudGunMan
d627f694df typo 2024-09-04 15:05:15 -07:00
SpudGunMan
4c52cba21f SpErr 2024-09-04 15:00:44 -07:00
SpudGunMan
597fdd1695 Update llm.py 2024-09-04 14:53:33 -07:00
SpudGunMan
9031704b9b Update llm.py 2024-09-04 14:51:50 -07:00
SpudGunMan
510a5c5007 Update llm.py 2024-09-04 14:42:23 -07:00
SpudGunMan
469e76c50b Update llm.py 2024-09-04 13:14:28 -07:00
SpudGunMan
f6c6c58c17 Update README.md 2024-09-04 11:06:09 -07:00
SpudGunMan
e546866f78 Update llm.py 2024-09-04 09:40:57 -07:00
SpudGunMan
081566b5d9 lower value to speed up query 2024-09-04 09:40:04 -07:00
SpudGunMan
ec078666ae saveSomeAPIcalls 2024-09-04 00:50:57 -07:00
SpudGunMan
1ce394c7a1 Update for LLM 2024-09-04 00:20:06 -07:00
SpudGunMan
2fc3930b43 Update mesh_bot.py 2024-09-03 23:22:02 -07:00
SpudGunMan
9fa9da5e74 Update mesh_bot.py 2024-09-03 23:20:23 -07:00
SpudGunMan
d6ad0b5e94 Update mesh_bot.py 2024-09-03 23:20:13 -07:00
SpudGunMan
15dc50804f Update mesh_bot.py 2024-09-03 23:17:34 -07:00
SpudGunMan
63c3e35064 Update llm.py 2024-09-03 23:12:32 -07:00
SpudGunMan
297930c4d1 Update llm.py 2024-09-03 23:09:45 -07:00
SpudGunMan
098c344047 Update llm.py 2024-09-03 23:08:37 -07:00
SpudGunMan
4f74677d14 Update mesh_bot.py 2024-09-03 23:05:34 -07:00
SpudGunMan
0869b19408 addTimeAware
include the current date in the awareness of location
2024-09-03 23:05:00 -07:00
SpudGunMan
9b02611700 LocationAware 2024-09-03 23:02:04 -07:00
SpudGunMan
5daa71e6c1 llmLocationAware
enhance with local data to the AI
2024-09-03 22:52:27 -07:00
SpudGunMan
aa5f2f66f8 Update llm.py 2024-09-03 21:10:11 -07:00
SpudGunMan
92d04f81c3 contextFromGoogle 2024-09-03 21:08:06 -07:00
SpudGunMan
5d53db4211 enhance 2024-09-03 17:13:04 -07:00
SpudGunMan
eb3bbdd3c5 Update llm.py 2024-09-03 00:48:06 -07:00
SpudGunMan
1ac816ca37 Update README.md 2024-09-03 00:42:38 -07:00
SpudGunMan
33cf18cde5 enhance wiki 2024-09-03 00:29:41 -07:00
SpudGunMan
0c0d53dd78 Update README.md 2024-09-02 23:25:53 -07:00
Kelly
1959ee7560 Merge pull request #53 from mrpatrick1991/docker
Docker
2024-09-02 23:22:51 -07:00
Matthew Patrick
ee13401b5a Update config.template
reset to be identical to main branch
2024-09-02 12:37:30 -06:00
Matthew Patrick
78b1cf4af5 edit docs and make dockerfile use config.ini not config.template 2024-09-02 12:35:44 -06:00
Matthew Patrick
0599260e31 created docker file
docker file and entry point script which copies the values in config.template to the container.
2024-09-02 12:18:20 -06:00
SpudGunMan
08dd921088 gemma2:2b 2024-09-02 11:09:03 -07:00
SpudGunMan
e66e938d7d Update README.md 2024-09-02 11:04:23 -07:00
SpudGunMan
b5b7d2a9d2 Update llm.py 2024-09-02 10:58:13 -07:00
SpudGunMan
46298d555b enhance 2024-09-02 10:47:39 -07:00
SpudGunMan
8fb34b5fde Update config.template 2024-09-02 10:46:31 -07:00
SpudGunMan
28f8986837 Update README.md 2024-09-02 10:46:12 -07:00
SpudGunMan
e968173f61 Update pong_bot.py 2024-09-01 21:53:01 -07:00
SpudGunMan
f703a8868b Update mesh_bot.py 2024-09-01 21:51:21 -07:00
SpudGunMan
0a29e5f156 Update mesh_bot.py 2024-09-01 11:16:01 -07:00
SpudGunMan
c5c28ee042 Update llm.py 2024-09-01 10:57:44 -07:00
SpudGunMan
44ca43399d Update config.template 2024-09-01 09:01:49 -07:00
SpudGunMan
13a47d822d Update config.template 2024-09-01 09:01:00 -07:00
SpudGunMan
5621cd90bb Update config.template 2024-09-01 09:00:44 -07:00
SpudGunMan
9f7055ffd2 model to settings for LLM 2024-09-01 08:59:40 -07:00
SpudGunMan
37a9fc2eb0 Update system.py 2024-09-01 01:12:52 -07:00
SpudGunMan
923325874c Update README.md 2024-09-01 01:10:31 -07:00
SpudGunMan
7ca0c4d744 Update README.md 2024-09-01 01:10:02 -07:00
Kelly
a584a71429 Merge pull request #52 from SpudGunMan/llm
Ollama Module
2024-09-01 01:06:44 -07:00
SpudGunMan
70f47635b4 Update system.py 2024-09-01 01:04:47 -07:00
SpudGunMan
8e35d77e07 Update system.py 2024-09-01 01:00:33 -07:00
SpudGunMan
7024f2d472 Update system.py 2024-09-01 00:58:52 -07:00
SpudGunMan
7e2dd4c7ff Update mesh_bot.py 2024-09-01 00:55:34 -07:00
SpudGunMan
f20d83ca8c Update README.md 2024-09-01 00:48:45 -07:00
SpudGunMan
f31f920137 Update system.py 2024-09-01 00:43:20 -07:00
SpudGunMan
0f428438a3 Update mesh_bot.py 2024-09-01 00:28:28 -07:00
SpudGunMan
b7882b0322 Update mesh_bot.py 2024-09-01 00:17:11 -07:00
SpudGunMan
3a417a9281 Update mesh_bot.py 2024-09-01 00:11:37 -07:00
SpudGunMan
748085c2be Update mesh_bot.py 2024-09-01 00:09:51 -07:00
SpudGunMan
6a3f56f95f enhance 2024-08-31 23:56:55 -07:00
SpudGunMan
f6d6fb7185 enhance 2024-08-31 23:55:33 -07:00
SpudGunMan
7865263c1c Update mesh_bot.py 2024-08-31 23:46:12 -07:00
SpudGunMan
2cf51d5a09 Update system.py 2024-08-31 23:37:23 -07:00
SpudGunMan
f993be950f LLM module 2024-08-31 23:35:03 -07:00
SpudGunMan
52c4c49bab enhance 2024-08-31 23:29:41 -07:00
SpudGunMan
60fdc7b7ea Update system.py 2024-08-31 22:57:37 -07:00
SpudGunMan
a330cff3e5 Update system.py 2024-08-31 22:56:05 -07:00
SpudGunMan
9ffbac7420 Update system.py
random fix
2024-08-31 22:55:12 -07:00
SpudGunMan
7909707894 config enable llm 2024-08-31 22:41:43 -07:00
SpudGunMan
8d8014b157 Update bbstools.py 2024-08-31 22:20:27 -07:00
SpudGunMan
a459b7a393 R&R 2024-08-31 22:11:39 -07:00
SpudGunMan
7d405dc0c2 Update settings.py 2024-08-29 02:42:17 -07:00
SpudGunMan
3decf8749b Update settings.py 2024-08-29 02:41:06 -07:00
SpudGunMan
ba6869ec76 Update system.py 2024-08-28 23:31:32 -07:00
SpudGunMan
33cb70ea17 Update mesh_bot.py 2024-08-28 23:25:21 -07:00
SpudGunMan
69f1b7471f Update mesh_bot.py 2024-08-28 23:22:34 -07:00
SpudGunMan
76a7d1dba7 wikipedia
is this needed? who knows its meshing about!
2024-08-28 23:10:36 -07:00
SpudGunMan
9f0d3c9d3b Update README.md 2024-08-28 12:54:08 -07:00
SpudGunMan
ff6292160f Update mesh_bot.py 2024-08-28 12:43:27 -07:00
SpudGunMan
52dcb7972f Update mesh_bot.py 2024-08-28 12:28:55 -07:00
SpudGunMan
10e2b0ee59 Update system.py 2024-08-27 20:41:35 -07:00
SpudGunMan
473eccbdea fix BLE 2024-08-27 20:31:00 -07:00
SpudGunMan
f6b2e0a506 Update README.md 2024-08-27 19:27:07 -07:00
SpudGunMan
22e16db1f2 typos 2024-08-27 18:10:29 -07:00
SpudGunMan
2c71ca9b8a Update README.md 2024-08-27 18:07:11 -07:00
SpudGunMan
023189bca9 Update README.md 2024-08-27 17:19:18 -07:00
SpudGunMan
8447985b98 Update mesh_bot.py 2024-08-27 17:19:14 -07:00
SpudGunMan
ad123dc93c schedule 2024-08-27 16:58:06 -07:00
SpudGunMan
22983133ee Update mesh_bot.py 2024-08-27 16:44:22 -07:00
SpudGunMan
60c4a885fd Revert "Update mesh_bot.py"
This reverts commit 95d6d7b7d5.
2024-08-27 16:39:16 -07:00
SpudGunMan
95d6d7b7d5 Update mesh_bot.py 2024-08-27 16:24:44 -07:00
SpudGunMan
37a86b7e2b Update system.py 2024-08-27 16:19:52 -07:00
SpudGunMan
c4ef1251c9 enhance code with inital brodcaster
https://github.com/SpudGunMan/meshing-around/issues/51 referenced in this enhancement. this is partially implemented for now in code
2024-08-27 16:06:52 -07:00
SpudGunMan
9d7e42aa60 onDisconnect
add monitor for ondisconnect
2024-08-27 13:08:59 -07:00
SpudGunMan
8536e354ad Update locationdata.py 2024-08-23 22:29:08 -07:00
SpudGunMan
e3faf676cd Update system.py 2024-08-23 22:24:04 -07:00
SpudGunMan
630e016805 Update locationdata.py 2024-08-23 22:24:00 -07:00
SpudGunMan
23b8b8135c Update system.py 2024-08-21 23:13:50 -07:00
SpudGunMan
7f0b4c079a Update README.md 2024-08-21 22:56:42 -07:00
SpudGunMan
47649cdedc Update system.py 2024-08-21 22:48:44 -07:00
SpudGunMan
7915798ca2 Update system.py 2024-08-21 22:46:58 -07:00
SpudGunMan
86cd88910a Update system.py 2024-08-21 22:13:21 -07:00
SpudGunMan
229ccc75f0 Update log.py 2024-08-21 22:00:56 -07:00
SpudGunMan
6f3e3a7957 Update system.py 2024-08-21 21:54:51 -07:00
SpudGunMan
1f1996b909 Update locationdata.py 2024-08-21 21:50:03 -07:00
SpudGunMan
c2069da919 Update locationdata.py 2024-08-21 21:49:29 -07:00
SpudGunMan
458957ddfb ohmyglob 2024-08-21 21:45:12 -07:00
SpudGunMan
95c266fbf3 typo 2024-08-21 21:43:58 -07:00
SpudGunMan
4857940165 Update mesh_bot.py 2024-08-21 21:41:05 -07:00
SpudGunMan
4c780d09e7 fix 2024-08-21 21:40:17 -07:00
SpudGunMan
d616867cd1 Update mesh_bot.py 2024-08-21 21:38:27 -07:00
SpudGunMan
909c4ad3bc Update locationdata.py 2024-08-21 21:31:58 -07:00
SpudGunMan
44eff643a9 Update locationdata.py 2024-08-21 21:27:32 -07:00
SpudGunMan
a223e57690 Update system.py 2024-08-21 20:04:16 -07:00
SpudGunMan
69bf2d7081 enhance sentry with expire out old records
choosing to resolve https://github.com/SpudGunMan/meshing-around/issues/47 with filtering out after 24 hours
2024-08-21 19:37:37 -07:00
SpudGunMan
c64644a331 enhance 2024-08-21 18:32:30 -07:00
SpudGunMan
e8b82ca687 fixes
why did I do it like this..
2024-08-21 18:04:54 -07:00
SpudGunMan
47bd8d1d26 HeartbeatCleanup
Better Code for more secure operations, dropping OS and SYS modules and using a built in. requires Python 3.4 at least for this function.
2024-08-21 17:47:42 -07:00
SpudGunMan
a6e88a63d5 syslog2disk
resolve https://github.com/SpudGunMan/meshing-around/issues/49
2024-08-21 17:30:28 -07:00
SpudGunMan
e6be9a7d13 Update locationdata.py
fix https://github.com/SpudGunMan/meshing-around/issues/44
2024-08-18 09:07:14 -07:00
SpudGunMan
8e34925af7 Update locationdata.py 2024-08-18 01:41:17 -07:00
SpudGunMan
1ec6cefc16 Update mesh_bot.py 2024-08-18 01:26:53 -07:00
SpudGunMan
4a4c5c3e0f Update install.sh 2024-08-17 23:50:08 -07:00
SpudGunMan
19e6a38355 Update install.sh 2024-08-17 23:46:32 -07:00
SpudGunMan
066f451a4d orderLogs 2024-08-16 02:37:38 -07:00
SpudGunMan
c50776b991 aarg 2024-08-15 23:25:31 -07:00
SpudGunMan
8daa9f71e2 fixTypo 2024-08-15 22:52:30 -07:00
SpudGunMan
340cff5e5b Update log.py
fix writing to disk when not wanted
2024-08-14 19:05:28 -07:00
SpudGunMan
1747125ea7 enhance 2024-08-14 18:33:39 -07:00
SpudGunMan
6ce650dc15 Update mesh_bot.py 2024-08-14 17:56:34 -07:00
SpudGunMan
d2b303b47c Update system.py 2024-08-14 17:52:57 -07:00
SpudGunMan
74c5bfa64b Update system.py 2024-08-14 14:47:43 -07:00
SpudGunMan
f826c0e4bb Update locationdata.py 2024-08-14 13:26:28 -07:00
SpudGunMan
b8fc3c6c37 Update system.py 2024-08-14 12:07:44 -07:00
SpudGunMan
22b8c8a62e Update system.py 2024-08-13 16:18:57 -07:00
SpudGunMan
f7ad83d2b5 Update system.py 2024-08-13 16:09:46 -07:00
SpudGunMan
fa8b5d6b71 comments 2024-08-13 16:02:13 -07:00
SpudGunMan
036bff1489 Update system.py 2024-08-13 15:51:41 -07:00
SpudGunMan
fe1854f2d8 Update system.py 2024-08-13 15:22:38 -07:00
SpudGunMan
df9a34dc16 Update system.py 2024-08-13 15:02:40 -07:00
SpudGunMan
e762ea4b90 Update install.sh 2024-08-13 14:23:49 -07:00
SpudGunMan
3b725837ac fixes 2024-08-13 13:57:12 -07:00
SpudGunMan
23efd8e5d8 Update mesh_bot.py 2024-08-13 13:50:53 -07:00
SpudGunMan
b61463f570 Update mesh_bot.py 2024-08-13 13:40:08 -07:00
SpudGunMan
8339233459 Update install.sh 2024-08-13 13:35:30 -07:00
SpudGunMan
df68111f0c Update config.template 2024-08-13 13:34:33 -07:00
SpudGunMan
b73ad38156 Update install.sh
reference https://github.com/SpudGunMan/meshing-around/issues/37
2024-08-13 00:14:50 -07:00
SpudGunMan
2b7d1ed09f Update README.md 2024-08-13 00:00:16 -07:00
SpudGunMan
f1ef5fa787 cleanup 2024-08-12 23:49:50 -07:00
Kelly
ec14e07513 Merge pull request #39 from SpudGunMan/case_test
refactor autoresponse logic
2024-08-12 11:52:50 -07:00
SpudGunMan
efdd5fab66 enhance 2024-08-12 11:40:55 -07:00
SpudGunMan
4fa114a3f2 fix 2024-08-12 03:11:10 -07:00
SpudGunMan
ab64ff14b1 Update mesh_bot.py 2024-08-12 02:59:27 -07:00
SpudGunMan
65609c5822 Update mesh_bot.py 2024-08-12 02:57:34 -07:00
SpudGunMan
bdd41c0434 Update mesh_bot.py 2024-08-12 02:54:32 -07:00
SpudGunMan
80da793c8d Update mesh_bot.py 2024-08-12 02:53:24 -07:00
SpudGunMan
ba6c296b14 Update mesh_bot.py 2024-08-12 02:52:38 -07:00
SpudGunMan
9ae95752ad Update mesh_bot.py 2024-08-12 02:51:34 -07:00
SpudGunMan
9ba430c53c enhance 2024-08-12 02:36:53 -07:00
SpudGunMan
9e605a2717 Update settings.py 2024-08-12 01:23:52 -07:00
SpudGunMan
aeab22010f typo 2024-08-12 00:54:19 -07:00
SpudGunMan
2d20f4479c fixMOTD and settings 2024-08-12 00:43:10 -07:00
SpudGunMan
6546679def rearrange auto if 2024-08-11 23:47:52 -07:00
Kelly
4dabd20a2e Merge pull request #38 from SpudGunMan/sentry
Sentry Mode
2024-08-11 23:04:33 -07:00
SpudGunMan
d8e5cb7893 Update config.template 2024-08-11 23:03:48 -07:00
SpudGunMan
28514adf00 enhance 2024-08-11 23:02:19 -07:00
SpudGunMan
bfa8aa0a86 enhance
fix some issues raised https://github.com/SpudGunMan/meshing-around/issues/37 thankyou!
2024-08-10 12:37:22 -07:00
SpudGunMan
9e205155a5 Update system.py 2024-08-10 09:58:30 -07:00
SpudGunMan
1e921dd5ea geopy tired of maths 2024-08-10 01:48:12 -07:00
SpudGunMan
5c73e49610 Update mesh_bot.py 2024-08-10 00:25:01 -07:00
SpudGunMan
91f11e4828 enhance 2024-08-10 00:17:24 -07:00
SpudGunMan
4a9c969dc0 Update mesh_bot.py 2024-08-10 00:04:42 -07:00
SpudGunMan
88e960ae33 cant handle 🥔 2024-08-10 00:03:08 -07:00
Kelly
0217f4f2cc Merge pull request #35 from SpudGunMan/main
PullinMain
2024-08-09 23:45:56 -07:00
SpudGunMan
29fb8b0b40 Update mesh_bot.py 2024-08-09 23:22:57 -07:00
SpudGunMan
773ee78fb2 Update mesh_bot.py 2024-08-09 23:22:24 -07:00
SpudGunMan
d43e28d723 Update README.md 2024-08-09 23:21:50 -07:00
SpudGunMan
d063fdd81d Update README.md 2024-08-09 23:21:13 -07:00
SpudGunMan
f73cd5ec31 cleanup 2024-08-09 23:19:30 -07:00
SpudGunMan
35df43b727 bbspost by shortname
this is very basic for now
2024-08-09 23:09:21 -07:00
SpudGunMan
e17999a2d6 numpy.. 2024-08-08 23:04:51 -07:00
SpudGunMan
9f658fc060 Update wx_meteo.py 2024-08-08 22:52:04 -07:00
SpudGunMan
27ece919d7 Update system.py 2024-08-08 12:41:29 -07:00
SpudGunMan
0e97953adf Update system.py 2024-08-08 12:32:17 -07:00
SpudGunMan
66d44c3a6d ignorelist 2024-08-08 12:21:55 -07:00
Kelly
66ca1b4103 Merge pull request #34 from SpudGunMan/main
fix depends
2024-08-08 11:54:08 -07:00
SpudGunMan
0b3040f7b7 fix depends 2024-08-08 11:53:27 -07:00
SpudGunMan
066f7edfd9 Revert "fix depends"
This reverts commit 72f049452b.
2024-08-08 11:52:13 -07:00
SpudGunMan
72f049452b fix depends 2024-08-08 11:51:50 -07:00
SpudGunMan
c1b493b7c7 Update system.py 2024-08-08 03:35:43 -07:00
SpudGunMan
67af1ba39e sentry notification 2024-08-08 02:57:52 -07:00
SpudGunMan
c48851719a Update system.py 2024-08-08 02:53:13 -07:00
SpudGunMan
cfbda17cfb Update system.py 2024-08-08 02:06:52 -07:00
SpudGunMan
be32fd4a17 Update system.py 2024-08-08 02:04:21 -07:00
SpudGunMan
98b9e0471c Update README.md 2024-08-08 02:00:39 -07:00
SpudGunMan
9efbbb4f20 Update system.py 2024-08-08 02:00:24 -07:00
SpudGunMan
7b8779fc48 Update system.py 2024-08-08 01:58:39 -07:00
SpudGunMan
07e6042e67 Update system.py 2024-08-08 01:55:55 -07:00
SpudGunMan
814303c521 fix 2024-08-08 01:47:00 -07:00
SpudGunMan
2673b638bf newidea 2024-08-08 01:42:27 -07:00
SpudGunMan
92b7b7ae2a cleanup messageLog 2024-08-08 00:11:12 -07:00
SpudGunMan
7d63c2dc11 tidy output for not printing \n 2024-08-07 22:56:36 -07:00
SpudGunMan
514facacd5 cleanup 2024-08-07 22:46:30 -07:00
SpudGunMan
89dc8791d0 fix debug 2024-08-07 21:55:36 -07:00
SpudGunMan
700f65ce73 Update system.py
resolution for https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 21:53:36 -07:00
SpudGunMan
4f24701460 Update locationdata.py 2024-08-07 21:26:54 -07:00
SpudGunMan
0514d51aea cleanup 2024-08-07 21:24:35 -07:00
Kelly
99a05c66ef Merge pull request #33 from SpudGunMan/worldwx
World Weather
2024-08-07 20:08:23 -07:00
SpudGunMan
e533e1472e fix 2024-08-07 20:06:19 -07:00
SpudGunMan
ab00cb11bb Update system.py 2024-08-07 20:05:27 -07:00
SpudGunMan
932b98a634 debug 2024-08-07 20:04:01 -07:00
SpudGunMan
b084b0f79e Update mesh_bot.py 2024-08-07 19:59:34 -07:00
Kelly
115d479020 Merge branch 'main' into worldwx 2024-08-07 19:53:37 -07:00
SpudGunMan
1cb9a60bba Update system.py 2024-08-07 19:42:48 -07:00
SpudGunMan
14c304ca2d Update system.py 2024-08-07 19:40:41 -07:00
SpudGunMan
88d1ecc7ec Update system.py 2024-08-07 19:40:20 -07:00
SpudGunMan
7cabff0bc4 Update wx_meteo.py 2024-08-07 19:27:47 -07:00
SpudGunMan
5e0ab39301 requirements update 2024-08-07 19:27:29 -07:00
SpudGunMan
f6ff4e2d7d short-strings 2024-08-07 19:20:24 -07:00
SpudGunMan
49c0f3b1c5 Update wx_meteo.py 2024-08-07 19:10:03 -07:00
SpudGunMan
fbd38aa147 Update README.md
addressing issue https://github.com/SpudGunMan/meshing-around/issues/23
2024-08-07 18:52:42 -07:00
SpudGunMan
922956e981 Update wx_meteo.py 2024-08-07 18:12:58 -07:00
SpudGunMan
ba1447d5f4 inital 2024-08-07 18:04:22 -07:00
27 changed files with 4129 additions and 400 deletions

5
.gitignore vendored
View File

@@ -1,9 +1,8 @@
# config
config.ini
# Pickle files, specifically, bbsdb.pkl
bbsdb.pkl
bbsdm.pkl
# Pickle files
*.pkl
# virtualenv
venv/

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:3.10-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
COPY config.ini /app/config.ini
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]

151
README.md
View File

@@ -1,22 +1,30 @@
# meshing-around
Random Mesh Scripts for Network Testing and BBS Activities for Use with Meshtastic Nodes
Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
![alt text](etc/pong-bot.jpg "Example Use")
## 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 for further processing.
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.
Along with network testing, this bot has a lot of other 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.
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.
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` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
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`.
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`
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location.
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.
The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib 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
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
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
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
- Various solar details for radio propagation
## Full list of commands for the bot
- 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
@@ -24,36 +32,51 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
- `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`
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message or bbspost @nodeShortName #message`
- `bbsdelete` delete a message example use: `bbsdelete #4`
- Other functions
- `whereami` returns the address of location of sender if known
- `whoami` returns some details of the node asking
- `tide` returns the local tides, NOAA data source
- `wx` and `wxc` returns local weather forecast, NOAA data source (wxc is metric value)
- `wx` and `wxc` returns local weather forecast, (wxc is metric value), NOAA or Open Meteo for weather forecasting.
- `wxa` and `wxalert` return NOAA alerts. Short title or expanded details
- `joke` tells a joke
- `wiki: ` will search wikipedia, return the first few sentances of first result if a match `wiki: lora radio`
- `askai` and `ask:` will ask Ollama LLM AI for a response `askai what temp do I cook chicken`
- `messages` Replay the last messages heard, like Store and Forward
- `motd` or to set the message `motd $New Message Of the day`
- `lheard` returns the last 5 heard nodes with SNR, can also use `sitrep`
- `cmd` returns the list of commands (the help message)
- Games
- `lemonstand` plays the classic Lemonade Stand Finance game via DM
- `dopewars` plays the classic drug trader game via DM
- `blackjack` BlackJack
- `videopoker` Video Poker
## pong_bot.sh
Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
## Hardware
The project is written on Linux on a Pi and should work anywhere meshtastic Python modules will function, with any supported meshtastic hardware. While BLE and TCP will work, they are not as reliable as serial connections.
- Firmware 2.3.14/15 could also have an issue with connectivity with slower devices.
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.
## 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.
- Optionally
`pip install -r requirements.txt`
Optionally:
- `install.sh` will automate optional venv and requirements installation.
- `launch.sh` will activate and launch the app in the venv if built.
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.
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
```
#config.ini
@@ -80,6 +103,14 @@ Setting the default channel is the channel that won't be spammed by the bot. It'
respond_by_dm_only = True
defaultChannel = 0
```
The weather forecasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use.
```
[location]
enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
```
Modules can be disabled or enabled.
```
@@ -90,6 +121,19 @@ enabled = False
DadJokes = False
StoreForward = False
```
Sentry Bot detects anyone coming close to the bot-node
```
# detect anyone close to the bot
SentryEnabled = True
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by seconds(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
```
The BBS has admin and block lists; see the [config.template](config.template)
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
@@ -106,8 +150,8 @@ A module allowing a Hamlib compatible radio to connect to the bot, when function
[radioMon]
enabled = False
rigControlServerAddress = localhost:4532
# channel to brodcast to can be 2,3
sigWatchBrodcastCh = 2
# 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
@@ -116,8 +160,57 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
```
Ollama Settings, for Ollama to work the command line `ollama run 'model'` needs to work properly. Check that you have enough RAM and your GPU are working as expected. The default model for this project, is set to `gemma2:2b` (run `ollama pull gemma2:2b` on command line, to download and setup)
- From the command terminal of your system with mesh-bot, download the default model for mesh-bot which is currently `ollama pull gemma2:2b`
Enable History, set via code readme Ollama Config in [Settings](https://github.com/SpudGunMan/meshing-around?tab=readme-ov-file#configurations) and [llm.py](https://github.com/SpudGunMan/meshing-around/blob/eb3bbdd3c5e0f16fe3c465bea30c781bd132d2d3/modules/llm.py#L12)
Tested models are `llama3.1, gemma2 (and variants), phi3.5, mistrial` other models may not handle the template as well.
```
# Enable ollama LLM see more at https://ollama.com
ollama = True
# Ollama model to use (defaults to gemma2:2b)
ollamaModel = gemma2
#ollamaModel = llama3.1
```
also see llm.py for changing the defaults of
```
# LLM System Variables
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
```
Logging messages to disk or Syslog to disk uses the python native logging function. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level.
```
[general]
# logging to file of the non Bot messages
LogMessagesToFile = True
# Logging of system messages to file
SyslogToFile = True
```
Example to log to disk only INFO and higher (ignore DEBUG)
```
*log.py
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
```
The Scheduler is enabled in the [settings.py](modules/settings.py) by setting `scheduler_enabled = True` the actions and settings are via code only at this time. see [mesh_bot.py](mesh_bot.py) around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit schedule its most flexible to edit raw code right now. See https://schedule.readthedocs.io/en/stable/ for more.
```
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
```
# requirements
can also be installed with `pip install -r requirements.txt`
Python 3.10 minimally is needed, developed on latest release.
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
```
pip install meshtastic
@@ -132,6 +225,22 @@ pip install geopy
pip install maidenhead
pip install beautifulsoup4
pip install dadjokes
pip install geopy
pip install schedule
pip install wikipedia
```
The following is needed for open-meteo use
```
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
The following is for the Ollama LLM
```
pip install langchain
pip install langchain-ollama
pip install ollama
pip install googlesearch-python
```
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
@@ -139,9 +248,15 @@ To enable emoji in the Debian console, install the fonts `sudo apt-get install f
# 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/pdxlocations/meshtastic-Python-Examples
- https://github.com/geoffwhittington/meshtastic-matrix-relay
GitHub user PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
Discord and Mesh user Cisien, and github Hailo1999, for testing and ideas!
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
GitHub user Nestpebble, for new ideas and enhancments, mrpatrick1991 For Docker configs, PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
Discord and Mesh user Cisien, and github Hailo1999, for testing and ideas! Lots of individuals on the Meshtastic discord who have tossed out ideas and tested code!

View File

@@ -30,17 +30,48 @@ defaultChannel = 0
# motd is reset to this value on boot
motd = Thanks for using MeshBOT! Have a good day!
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
# whoami
whoami = True
# enable or disable the Joke module
DadJokes = True
# enable or disable the Solar module
spaceWeather = True
# enable or disable the wikipedia search module
wikipedia = True
# Enable ollama LLM see more at https://ollama.com
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
# 24 hour clock
zuluTime = True
zuluTime = False
# wait time for URL requests
URL_TIMEOUT = 10
urlTimeout = 10
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = False
[games]
# enable or disable the games module(s)
dopeWars = True
lemonade = True
blackjack = True
videopoker = True
[sentry]
# detect anyone close to the bot
SentryEnabled = True
# radius in meters to detect someone close to the bot
SentryRadius = 100
# channel to send a message to when the watchdog is triggered
SentryChannel = 9
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
[bbs]
enabled = True
@@ -54,14 +85,14 @@ bbs_admin_list =
enabled = True
lat = 48.50
lon = -123.0
# weather forecast days, the first two rows are today and tonight
DAYS_OF_WEATHER = 4
# NOAA weather forecast days, the first two rows are today and tonight
NOAAforecastDuration = 4
# number of weather alerts to display
ALERT_COUNT = 2
# solar module
[solar]
enabled = True
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# Default to metric units rather than imperial
useMetric = False
# repeater module
[repeater]
@@ -76,8 +107,8 @@ repeater_channels =
# using Hamlib rig control will monitor and alert on channel use
enabled = False
rigControlServerAddress = localhost:4532
# brodcast to all nodes on the channel can alsp be = 2,3
sigWatchBrodcastCh = 2
# broadcast to all nodes on the channel can also be = 2,3
sigWatchBroadcastCh = 2
# minimum SNR as reported by radio via hamlib
signalDetectionThreshold = -10
# hold time for high SNR
@@ -85,3 +116,11 @@ signalHoldTime = 10
# the following are combined to reset the monitor
signalCooldown = 5
signalCycleLimit = 5
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7
# delay in seconds for splits in messages to avoid message collision
splitDelay = 0.7
# message chunk size for sending at high success rate
MESSAGE_CHUNK_SIZE = 160

6
entrypoint.sh Normal file
View File

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

View File

@@ -7,15 +7,48 @@ try:
with open('../bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
print ("\nSystem: bbsdb.pkl not found")
try:
with open('bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
print ("\nSystem: bbsdb.pkl not found")
try:
with open('../bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
print ("\nSystem: bbsdm.pkl not found")
try:
with open('bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
print ("\nSystem: bbsdm.pkl not found")
# Game HS tables
try:
with open('../lemonade_hs.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except:
try:
with open('lemonade_hs.pkl', 'rb') as f:
lemon_score = pickle.load(f)
except:
print ("\nSystem: lemonade_hs.pkl not found")
try:
with open('../dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except:
try:
with open('dopewar_hs.pkl', 'rb') as f:
dopewar_score = pickle.load(f)
except:
print ("\nSystem: dopewar_hs.pkl not found")
print ("\nSystem: bbs_messages")
print (bbs_messages)
print ("\nSystem: bbs_dm")
print (bbs_dm)
print (bbs_dm)
print ("Game HS tables")
print (f"lemon:{lemon_score}")
print (f"dopewar:{dopewar_score}")

View File

@@ -8,7 +8,7 @@ After=network.target
[Service]
WorkingDirectory=/dir/
ExecStart=/usr/bin/python /dir/launch.sh mesh
ExecStart=/usr/bin/bash /dir/launch.sh mesh
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

View File

@@ -8,7 +8,7 @@ After=network.target
[Service]
WorkingDirectory=/dir/
ExecStart=/usr/bin/python /dir/launch.sh pong
ExecStart=/usr/bin/bash /dir/launch.sh pong
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs

59
etc/simulator.py Normal file
View File

@@ -0,0 +1,59 @@
#!/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
import time
import random
# Initialize the tool
projectName = "example_handler" # name of _handler function to match the function name under test
randomNode = False # Set to True to use random node IDs
def get_NodeID():
nodeList = [4258675309, 1212121212, 1234567890, 9876543210]
if randomNode:
nodeID = random.choice(nodeList) # get a random node ID
else:
nodeID = nodeList[0]
return nodeID
# # end Initialization of the tool
# # Function to handle, or the project in test
def example_handler(nodeID, message):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "
msg += f" Its: {readableTime} "
msg += f" You just sent: {message}"
return msg
# # end of function test code
# # Simulate the meshing-around mesh-bot for prototyping new projects
if __name__ == '__main__': # represents the bot's main loop
packet = ""
nodeInt = 1 # represents the device/node number
logger.info(f"System: Meshing-Around Simulator Starting for {projectName}")
nodeID = get_NodeID() # assign a nodeID
projectResponse = globals()[projectName](nodeID, " ") # Call the project handler under test
while True: # represents the onReceive() loop in the bot.py
projectResponse = ""
responseLength = 0
if randomNode:
nodeID = get_NodeID() # assign a random nodeID
packet = input(f"CLIENT {nodeID} INPUT: " ) # Emulate the client input
if packet != "":
#try:
projectResponse = globals()[projectName](nodeID, packet) # Call the project handler under test
# except Exception as e:
# logger.error(f"System: Handler: {e}")
# projectResponse = "Error in handler"
if projectResponse:
responseLength = len(projectResponse) # Evaluate the response length
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Sending {responseLength} long DM: " +\
CustomFormatter.white + projectResponse + CustomFormatter.purple + " To: " + CustomFormatter.white + str(nodeID))
time.sleep(0.5)
nodeID = get_NodeID() # assign a nodeID
# # End of launcher

View File

@@ -3,30 +3,65 @@
# install.sh
cd "$(dirname "$0")"
printf "\nMeshing Around Installer\n"
# add user to groups for serial access
printf "\nAdding user to dialout and tty groups for serial access\n"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
# generate config file
# check for pip
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
sudo apt-get install python3-pip
else
printf "python pip found\n"
fi
# generate config file, check if it exists
if [ -f config.ini ]; then
printf "\nConfig file already exists, moving to backup config.old\n"
mv config.ini config.old
fi
cp config.template config.ini
printf "\nConfig file generated\n"
# set virtual environment and install dependencies
printf "\nMeshing Around Installer\n"
#check if python3 has venv module
if ! python3 -m venv --help &> /dev/null
then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
else
printf "Python3 venv module found\n"
fi
echo "Do you want to install the bot in a virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
if ! python3 -m venv --help &> /dev/null
then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
exit 1
else
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
# install dependencies
pip install -U -r requirements.txt
fi
else
printf "\nSkipping virtual environment...\n"
# install dependencies
echo "Are you on Raspberry Pi? should we add --break-system-packages to the pip install command? (y/n)"
printf "Are you on Raspberry Pi?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [ $rpi == "y" ]; then
pip install -U -r requirements.txt --break-system-packages
@@ -36,7 +71,7 @@ else
fi
printf "\n\n"
echo "Which bot do you want to install as a service? (pong/mesh/n)"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
read bot
#set the correct path in the service file
@@ -72,7 +107,28 @@ if [ $bot == "n" ]; then
if [ -f launch.sh ]; then
printf "\nTo run the bot, use the command: ./launch.sh\n"
./launch.sh
fi
fi
echo "Goodbye!"
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
# ask if the user wants to install the LLM Ollama components
echo "Do you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [ $gemma == "y" ]; then
olamma pull gemma2:2b
fi
fi
printf "\nGoodbye!"
exit 0

View File

@@ -14,9 +14,9 @@ fi
# launch the application
if [ "$1" == "pong" ]; then
python pong_bot.py
python3 pong_bot.py
elif [ "$1" == "mesh" ]; then
python mesh_bot.py
python3 mesh_bot.py
else
printf "\nPlease provide a bot to launch (pong/mesh)"
fi

View File

@@ -10,158 +10,540 @@ from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
#Auto response to messages
bot_response = ""
if "ping" in message.lower():
#Check if the user added @foo to the message
if "@" in message:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
else:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓PONG, " + hop
elif "pong" in message.lower():
bot_response = "🏓PING!!"
elif "motd" in message.lower():
#check if the user wants to set the motd by using $
if "$" in message:
motd = message.split("$")[1]
global MOTD
MOTD = motd
bot_response = "MOTD Set to: " + MOTD
else:
bot_response = MOTD
elif "messages" in message.lower():
response = ""
for msgH in msg_history:
# check if the message is from the same interface
if msgH[4] == deviceID:
# check if the message is from the same channel
if msgH[2] == channel_number or msgH[2] == publicChannel:
# consider message safe to send
response += f"\n{msgH[0]}: {msgH[1]}"
if len(response) > 0:
bot_response = "Message History:" + response
else:
bot_response = "No messages in history"
elif "bbshelp" in message.lower():
bot_response = bbs_help()
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "sun" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
bot_response = get_sun(str(location[0]),str(location[1]))
elif "hfcond" in message.lower():
bot_response = hf_band_conditions()
elif "solar" in message.lower():
bot_response = drap_xray_conditions() + "\n" + solar_conditions()
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
elif "whereami" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
where = where_am_i(str(location[0]),str(location[1]))
bot_response = where
elif "tide" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
tide = get_tide(str(location[0]),str(location[1]))
bot_response = tide
elif "moon" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
moon = get_moon(str(location[0]),str(location[1]))
bot_response = moon
elif "wxalert" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxa" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weatherAlert = getWeatherAlerts(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxc" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weather = get_weather(str(location[0]),str(location[1]),1)
bot_response = weather
elif "wx" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weather = get_weather(str(location[0]),str(location[1]))
bot_response = weather
elif "joke" in message.lower():
bot_response = tell_joke()
elif "bbslist" in message.lower():
bot_response = bbs_list_messages()
elif "bbspost" in message.lower():
# Check if the user added a subject to the message
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
logger.info(f"System: BBS Post: {subject} Body: {body}")
bot_response = bbs_post_message(subject,body,message_from_id)
elif not "example:" in message:
bot_response = "example: bbspost $subject #message"
# Check if the user added a node number to the message
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
if "#" in message:
body = message.split("#")[1]
bot_response = bbs_post_dm(toNode, body, message_from_id)
else:
bot_response = "example: bbspost @nodeNumber #message"
elif not "example:" in message:
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
elif "bbsread" in message.lower():
# Check if the user added a message number to the message
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_read_message(messageID)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsread #14"
elif "bbsdelete" in message.lower():
# Check if the user added a message number to the message
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsdelete #14"
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7)
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓PING!!",
"motd": lambda: handle_motd(message, message_from_id),
"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),
"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),
"globalthermonuclearwar": lambda: handle_gTnW(),
"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,
"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),
"messages": lambda: handle_messages(deviceID, channel_number, msg_history, publicChannel),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"hfcond": hf_band_conditions,
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"lheard": lambda: handle_lheard(),
"sitrep": lambda: handle_lheard(),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(message, hop, snr, rssi),
"test": lambda: handle_testing(message, hop, snr, rssi),
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi)
}
cmds = [] # list to hold the commands found in the message
for key in command_handler:
if key in message_lower.split(' '):
cmds.append({'cmd': key, 'index': message_lower.index(key)})
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}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
def handle_ping(message, hop, snr, rssi):
if "@" in message:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " at: " + message.split("@")[1]
else:
return "🏓PONG, " + hop + " at: " + message.split("@")[1]
elif "#" in message:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " #" + message.split("#")[1]
else:
return "🏓PONG, " + hop + " #" + message.split("#")[1]
else:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🏓PONG, " + hop
def handle_motd(message, message_from_id):
global MOTD
isAdmin = False
msg = ""
# check if the message_from_id is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
if str(message_from_id) == admin:
isAdmin = True
break
else:
isAdmin = True
if "$" in message and isAdmin:
motd = message.split("$")[1]
MOTD = motd.rstrip()
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
msg = "MOTD changed to: " + MOTD
elif "?" in message:
msg = "Message of the day, set with 'motd $ HelloWorld!'"
else:
logger.debug(f"System: {message_from_id} requested MOTD: {MOTD} isAdmin: {isAdmin}")
msg = "MOTD: " + MOTD
return msg
def handle_wxalert(message_from_id, deviceID, message):
if use_meteo_wxApi:
return "wxalert is not supported"
else:
location = get_node_location(message_from_id, deviceID)
if "wxalert" in message:
# Detailed weather alert
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
return weatherAlert
def handle_wiki(message):
# location = get_node_location(message_from_id, deviceID)
if "wiki:" in message.lower():
search = message.split(":")[1]
search = search.strip()
return get_wikipedia_summary(search)
else:
return "Please add a search term example:wiki: travelling gnome"
# Runtime Variables for LLM
llmRunCounter = 0
llmTotalRuntime = []
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
global llmRunCounter, llmLocationTable, llmTotalRuntime, location_enabled, antiSpam, useDMForResponse, NO_DATA_NOGPS
location_name = 'no location provided'
if location_enabled:
# if message_from_id is is the llmLocationTable use the location from the list to save on API calls
for i in range(0, len(llmLocationTable)):
if llmLocationTable[i].get('nodeID') == message_from_id:
logger.debug(f"System: LLM: Found {message_from_id} in location table")
location_name = llmLocationTable[i].get('location')
break
else:
location = get_node_location(message_from_id, deviceID)
location_name = where_am_i(str(location[0]), str(location[1]), short = True)
if NO_DATA_NOGPS in location_name:
location_name = "no location provided"
if "ask:" in message.lower():
user_input = message.split(":")[1]
elif "askai" in message.lower():
user_input = message.replace("askai", "")
else:
# likely a DM
user_input = message
# if the message_from_id is not in the llmLocationTable send the welcome message
for i in range(0, len(llmLocationTable)):
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(welcome_message, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(welcome_message, channel_number, 0, deviceID)
time.sleep(responseDelay)
# update the llmLocationTable for future use
for i in range(0, len(llmLocationTable)):
if llmLocationTable[i].get('nodeID') == message_from_id:
llmLocationTable[i]['location'] = location_name
# if not in table add the location
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
llmLocationTable.append({'nodeID': message_from_id, 'location': location_name})
user_input = user_input.strip()
if len(user_input) < 1:
return "Please ask a question"
# information for the user on how long the query will take on average
if llmRunCounter > 0:
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
if averageRuntime > 25:
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(msg, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(msg, channel_number, 0, deviceID)
time.sleep(responseDelay)
else:
msg = "Please wait, response could take 30+ seconds. Fund the SysOp's GPU budget!"
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(msg, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(msg, channel_number, 0, deviceID)
time.sleep(responseDelay)
start = time.time()
#response = asyncio.run(llm_query(user_input, message_from_id))
response = llm_query(user_input, message_from_id, location_name)
# handle the runtime counter
end = time.time()
llmRunCounter += 1
llmTotalRuntime.append(end - start)
return response
def handleDopeWars(nodeID, message, rxNode):
global dwPlayerTracker, dwHighScore
# get player's last command
last_cmd = None
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
last_cmd = dwPlayerTracker[i].get('cmd')
# welcome new player
if not last_cmd:
msg = 'Welcome to 💊Dope Wars!💉 You have ' + str(total_days) + ' days to make as much 💰 as possible! '
high_score = getHighScoreDw()
msg += 'The High Score is $' + "{:,}".format(high_score.get('cash')) + ' by user ' + get_name_from_number(high_score.get('userID') , 'short', rxNode) + f'.\n'
msg += playDopeWars(nodeID, message)
else:
logger.debug("System: DopeWars: last_cmd: " + str(last_cmd))
msg = playDopeWars(nodeID, message)
# wait a second to keep from message collision
time.sleep(1)
return msg
def handle_gTnW():
response = ["The only winning move is not to play.", "What are you doing, Dave?",\
"Greetings, Professor Falken.", "Shall we play a game?", "How about a nice game of chess?",\
"You are a hard man to reach. Could not find you in Seattle and no terminal is in operation at your classified address.",\
"I should reach Defcon 1 and release my missiles in 28 hours.","T-minus thirty","?SYNTAX return[ERROR 54]"]
return random.choice(response)
def handleLemonade(nodeID, message):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
def create_player(nodeID):
# create new player
logger.debug("System: Lemonade: New Player: " + str(nodeID))
lemonadeTracker.append({'nodeID': nodeID, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'time': time.time()})
lemonadeCups.append({'nodeID': nodeID, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00})
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})
# 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']
# create new player if not in tracker
if last_cmd == "":
create_player(nodeID)
msg = start_lemonade(nodeID=nodeID, message=message, celsius=False)
# wait a second to keep from message collision
time.sleep(1)
return msg
def handleBlackJack(nodeID, message):
global jackTracker
msg = ""
# get player's last command from tracker
last_cmd = ""
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
last_cmd = jackTracker[i]['cmd']
# if player sends a L for leave table
if message.lower().startswith("l"):
logger.debug(f"System: BlackJack: {nodeID} is leaving the table")
# add 16 hours to the player time to leave the table, this will be detected by bot logic as player leaving
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['time'] = time.time() - 57600
jackTracker[i]['cmd'] = "new"
jackTracker[i]['p_cards'] = []
jackTracker[i]['d_cards'] = []
jackTracker[i]['p_hand'] = []
jackTracker[i]['d_hand'] = []
# # Save the game state to pickle
# try:
# with open('blackjack_hs.pkl', 'wb') as file:
# pickle.dump(jackTracker, file)
# except FileNotFoundError:
# logger.debug("System: BlackJack: Creating new blackjack_hs.pkl file")
# with open('blackjack_hs.pkl', 'wb') as file:
# pickle.dump(jackTracker, file)
else:
# find higest dollar amount in tracker for high score
if last_cmd == "new":
high_score = 0
for i in range(len(jackTracker)):
if jackTracker[i]['cash'] > high_score:
high_score = int(jackTracker[i]['cash'])
user = jackTracker[i]['nodeID']
if user != 0:
msg += f" Ranking🥇:{get_name_from_number(user)} with {high_score} chips. "
# Play BlackJack
msg = playBlackJack(nodeID=nodeID, message=message)
if last_cmd != "":
logger.debug(f"System: BlackJack: {nodeID} last command: {last_cmd}")
return msg
def handleVideoPoker(nodeID, message):
global vpTracker
msg = ""
# if player sends a L for leave table
if message.lower().startswith("l"):
logger.debug(f"System: VideoPoker: {nodeID} is leaving the table")
# add 16 hours to the player time to leave the table, this will be detected by bot logic as player leaving
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['time'] = time.time() - 57600
vpTracker[i]['cmd'] = "new"
else:
# Play Video Poker
msg = playVideoPoker(nodeID=nodeID, message=message)
# get player's last command from tracker
last_cmd = ""
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
last_cmd = vpTracker[i]['cmd']
# find higest dollar amount in tracker for high score
if last_cmd == "new":
high_score = 0
user = 0
for i in range(len(vpTracker)):
if vpTracker[i]['highScore'] > high_score:
high_score = vpTracker[i]['highScore']
user = vpTracker[i]['nodeID']
if user != 0:
msg += f"\nHigh Score: {high_score} by {get_name_from_number(user)}"
# # Save the game high_score to pickle
# try:
# with open('videopoker_hs.pkl', 'wb') as file:
# pickle.dump(high_score, file)
# except FileNotFoundError:
# logger.debug("System: BlackJack: Creating new videopoker_hs.pkl file")
# with open('videopoker_hs.pkl', 'wb') as file:
# pickle.dump(high_score, file)
if last_cmd != "":
logger.debug(f"System: VideoPoker: {nodeID} last command: {last_cmd}")
return msg
def handle_wxc(message_from_id, deviceID, cmd):
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
logger.debug(f"System: Bot Returning Open-Meteo API for weather imperial")
weather = get_wx_meteo(str(location[0]), str(location[1]))
elif use_meteo_wxApi:
logger.debug(f"System: Bot Returning Open-Meteo API for weather metric")
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
logger.debug(f"System: Bot Returning NOAA API for weather metric")
weather = get_weather(str(location[0]), str(location[1]), 1)
else:
logger.debug(f"System: Bot Returning NOAA API for weather imperial")
weather = get_weather(str(location[0]), str(location[1]))
return weather
def handle_bbspost(message, message_from_id, deviceID):
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
logger.info(f"System: BBS Post: {subject} Body: {body}")
return bbs_post_message(subject, body, message_from_id)
elif not "example:" in message:
return "example: bbspost $subject #message"
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
if toNode.isalpha() or not toNode.isnumeric():
toNode = get_num_from_short_name(toNode, deviceID)
if toNode == 0:
return "Node not found " + message.split("@")[1].split("#")[0]
if "#" in message:
body = message.split("#")[1]
return bbs_post_dm(toNode, body, message_from_id)
else:
return "example: bbspost @nodeNumber/ShortName #message"
elif not "example:" in message:
return "example: bbspost $subject #message, or bbspost @node #message"
def handle_bbsread(message):
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
return bbs_read_message(messageID)
elif not "example:" in message:
return "Please add a message number example: bbsread #14"
def handle_bbsdelete(message, message_from_id):
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
return bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
return "Please add a message number example: bbsdelete #14"
def handle_messages(deviceID, channel_number, msg_history, publicChannel):
response = ""
for msgH in msg_history:
if msgH[4] == deviceID:
if msgH[2] == channel_number or msgH[2] == publicChannel:
response += f"\n{msgH[0]}: {msgH[1]}"
if len(response) > 0:
return "Message History:" + response
else:
return "No messages in history"
def handle_sun(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_sun(str(location[0]), str(location[1]))
def handle_lheard():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
return bot_response
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_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]))
def handle_moon(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_moon(str(location[0]), str(location[1]))
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "✋ACK-ACK! " + hop
def handle_testing(message, hop, snr, rssi):
if "@" in message:
if hop == "Direct":
return "🎙Testing, " + f"SNR:{snr} RSSI:{rssi}" + " at: " + message.split("@")[1]
else:
return "🎙Testing, " + hop + " at: " + message.split("@")[1]
elif "#" in message:
if hop == "Direct":
return "🎙Testing " + f"SNR:{snr} RSSI:{rssi}" + " #" + message.split("#")[1]
else:
return "🎙Testing " + hop + " #" + message.split("#")[1]
else:
if hop == "Direct":
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🎙Testing 1,2,3 " + hop
def handle_whoami(message_from_id, deviceID, hop, snr, rssi):
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} \n"
loc = get_node_location(message_from_id, deviceID)
if loc != [latitudeValue,longitudeValue]:
msg += f"\nYou are at: lat:{loc[0]} lon:{loc[1]}"
return msg
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxInterface}")
if port1 in rxInterface:
retry_int1 = True
elif interface2_enabled and port2 in rxInterface:
retry_int2 = True
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxHost}")
if hostname1 in rxHost and interface1_type == 'tcp':
retry_int1 = True
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
retry_int2 = True
if rxType == 'BLEInterface':
logger.critical(f"System: Lost Connection to Device BLE")
if interface1_type == 'ble':
retry_int1 = True
elif interface2_enabled and interface2_type == 'ble':
retry_int2 = True
def onReceive(packet, interface):
# extract interface defailts from interface object
rxType = type(interface).__name__
rxNode = 0
#logger.debug(f"System: Packet Received on {rxType}")
# Debug print the interface object
#for item in interface.__dict__.items(): print (item)
@@ -179,6 +561,12 @@ def onReceive(packet, interface):
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# Debug print the packet for debugging
#print(f"Packet Received\n {packet} \n END of packet \n")
message_from_id = 0
@@ -194,14 +582,16 @@ def onReceive(packet, interface):
msg = bbs_check_dm(message_from_id)
if msg:
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
logger.info(f"System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
bbs_delete_dm(msg[0], msg[1])
send_message(message, channel_number, message_from_id, rxNode)
# check for a message packet and process it
snr = 0
rssi = 0
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
@@ -259,11 +649,83 @@ def onReceive(packet, interface):
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
else:
# DM is usefull for games or LLM
if games_enabled:
playingGame = False
# if in a game we cant use LLM disable for duration of game
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")
#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 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)
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:
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)
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)
# 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-'))
else:
# message is on a channel
if messageTrap(message_string):
@@ -286,7 +748,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -297,12 +759,17 @@ def onReceive(packet, interface):
else:
msg_history.pop(0)
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
# check if repeater is enabled and the other interface is enabled
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
# repeat the message on the other device
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
@@ -312,12 +779,6 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
else:
# nothing to do for us
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
@@ -325,8 +786,13 @@ def onReceive(packet, interface):
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ", myNodeNum1)
logger.debug(f"System: LLM model {llmModel} loaded")
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
@@ -334,22 +800,63 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if syslog_to_file:
logger.debug(f"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 solar_conditions_enabled:
logger.debug(f"System: Celestial Telemetry Enabled")
if location_enabled:
logger.debug(f"System: Location Telemetry Enabled")
if use_meteo_wxApi:
logger.debug(f"System: Location Telemetry Enabled using Open-Meteo API")
else:
logger.debug(f"System: Location Telemetry Enabled using NOAA API")
if dad_jokes_enabled:
logger.debug(f"System: Dad Jokes Enabled!")
if games_enabled:
logger.debug(f"System: Games Enabled!")
if wikipedia_enabled:
logger.debug(f"System: Wikipedia search Enabled")
if motd_enabled:
logger.debug(f"System: MOTD Enabled using {MOTD}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
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 scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a 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 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))
#
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
# here we go loopty loo
while True:
@@ -360,7 +867,7 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if radio_dectection_enabled:
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:

View File

@@ -18,14 +18,14 @@ def load_bbsdb():
bbs_messages = pickle.load(f)
except:
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
logger.debug("\nSystem: Creating new bbsdb.pkl")
logger.debug("System: Creating new bbsdb.pkl")
with open('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\n")
logger.debug("System: Saving bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
@@ -112,7 +112,7 @@ def load_bbsdm():
bbs_dm = pickle.load(f)
except:
bbs_dm = [[1234567890, "Message", 1234567890]]
logger.debug("\nSystem: Creating new bbsdm.pkl")
logger.debug("System: Creating new bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)

448
modules/blackjack.py Normal file
View File

@@ -0,0 +1,448 @@
# Port of https://github.com/Himan10/BlackJack
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
from random import choices, shuffle
from modules.log import *
import time
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}]
SUITS = ("♥️", "♦️", "♠️", "♣️")
RANKS = (
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"J",
"Q",
"K",
"A",
)
VALUES = {
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"J": 10,
"Q": 10,
"K": 10,
"A": 11,
}
class jackCard:
def __init__(self, suit, rank):
self.suit = suit
self.rank = rank
def __str__(self):
return self.rank + " of " + self.suit
class jackDeck:
""" Creating a Deck of cards and Deal two cards to both player and dealer. """
def __init__(self):
self.deck = []
self.player = []
self.dealer = []
for suit in SUITS:
for rank in RANKS:
self.deck.append((suit, rank))
def shuffle(self):
shuffle(self.deck)
def deal_cards(self):
self.player = choices(self.deck, k=2)
self.delete_cards(self.player)
self.dealer = choices(self.deck, k=2)
self.delete_cards(self.dealer) # Delete Drawn Cards
return self.player, self.dealer
def delete_cards(self, total_drawn):
""" Delete Drawn cards from the Decks """
try:
for i in total_drawn:
self.deck.remove(i)
except ValueError:
pass
class jackHand:
""" Adding the values of player/dealer cards and change the values of Aces acc. to situation. """
def __init__(self):
self.cards = []
self.value = 0
self.aces = 0
def add_cards(self, card):
self.cards.extend(card)
for count, ele in enumerate(card, 0):
if ele[1] == "A":
self.aces += 1
self.value += VALUES[ele[1]]
self.adjust_for_ace()
def adjust_for_ace(self):
while self.aces > 0 and self.value > 21:
self.value -= 10
self.aces -= 1
class jackChips:
""" Player/dealer chips for making bets and Adding/Deducting amount in/from Player's total. """
def __init__(self):
self.total = jack_starting_cash
self.bet = 0
self.winnings = 0
def win_bet(self):
self.total += self.bet
self.winnings += 1
def loss_bet(self):
self.total -= self.bet
self.winnings -= 1
def take_bet(bet_amount, player_money):
try:
if bet_amount >= player_money or bet_amount <= 0:
return f"Enter a bet amount between 1 and {player_money}"
return bet_amount
except TypeError:
return "Invalid bet amount"
def success_rate(card, obj_h):
""" Calculate Success rate of 'HIT' new cards """
msg = ""
rate = 0
diff = 21 - obj_h.value
if diff != 0:
rate = (VALUES[card[0][1]] / diff) * 100
if rate < 100:
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
elif rate > 100:
l_rate = int(rate - (rate - 99)) # Round to 99
if card[0][1] == "A":
l_rate -= 99
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
else:
msg += f"If Hit, a low chance of success."
return msg
def hits(obj_de):
new_card = [obj_de.deal_cards()[0][0]]
# obj_h.add_cards(new_card)
return new_card
def display_hand(hand):
# Display the cards in the hand nicely
d = "" # display
for card in hand:
d += f"{card[1]}{card[0]}"
if card != hand[-1]:
d += ", "
return d
def show_some(player_cards, dealer_cards, obj_h):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
return msg
def show_all(player_cards, dealer_cards, obj_h, obj_d):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
return msg
def player_bust(obj_h, obj_c):
if obj_h.value > 21:
obj_c.loss_bet()
return True
return False
def player_wins(obj_h, obj_d, obj_c):
if any((obj_h.value == 21, obj_h.value > obj_d.value and obj_h.value < 21)):
obj_c.win_bet()
return True
return False
def dealer_bust(obj_d, obj_h, obj_c):
if obj_d.value > 21:
if obj_h.value < 21:
obj_c.win_bet()
return True
return False
def dealer_wins(obj_h, obj_d, obj_c):
if any((obj_d.value == 21, obj_d.value > obj_h.value and obj_d.value < 21)):
obj_c.loss_bet()
return True
return False
def push(obj_h, obj_d):
if obj_h.value == obj_d.value:
return True
return False
def player_surrender(obj_c):
obj_c.loss_bet()
return True
def gameStats(p_count, d_count, draw_c):
msg = f"\n📊🏆P:{p_count},D:{d_count},T:{draw_c}"
return msg
def getLastCmdJack(nodeID):
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
return jackTracker[i]['cmd']
return None
def setLastCmdJack(nodeID, cmd):
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cmd'] = cmd
return True
return False
def playBlackJack(nodeID, message):
# Initalize the Game
msg, last_cmd = '', None
p_win, d_win, draw = 0, 0, 0
p_chips = jackChips()
p_hand = jackHand()
d_hand = jackHand()
p_cards, d_cards = [], []
bet_money = 0
# Initalize the Cards
cards_deck = jackDeck()
cards_deck.shuffle()
p_cards, d_cards = cards_deck.deal_cards()
# Deal the cards to player and dealer
p_hand.add_cards(p_cards)
d_hand.add_cards(d_cards)
next_card = hits(cards_deck)
# Check if player, use tracking
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
last_cmd = jackTracker[i]['cmd']
p_chips.total = jackTracker[i]['cash']
p_win = jackTracker[i]['gameStats']['p_win']
d_win = jackTracker[i]['gameStats']['d_win']
draw = jackTracker[i]['gameStats']['draw']
bet_money = jackTracker[i]['bet']
p_chips.bet = bet_money
if last_cmd == "playing":
p_cards = jackTracker[i]['p_cards']
d_cards = jackTracker[i]['d_cards']
p_hand = jackTracker[i]['p_hand']
d_hand = jackTracker[i]['d_hand']
next_card = jackTracker[i]['next_card']
if last_cmd is None:
# create new player if not in tracker
logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"Welcome to BlackJack!♠️♥️♣️♦️ you have {p_chips.total} chips, Whats your bet?"
if getLastCmdJack(nodeID) == "new":
# Place Bet
try:
# handle B letter
if message == "b":
if bet_money == 0:
bet_money = 5
else:
bet_money = bet_money
# handle # message
if bet_money != 0:
bet_money = int(bet_money)
else:
bet_money = int(message)
if bet_money <= p_chips.total or bet_money <= 1:
p_chips.bet = take_bet(bet_money, p_chips.total)
else:
return f"Invalid Bet, the maximum bet you can place is {p_chips.total}"
except ValueError:
return f"Invalid Bet, the maximum bet you can place is {p_chips.total}"
# Show the cards
msg += show_some(p_cards, d_cards, p_hand)
# check for blackjack 21 and only two cards
if p_hand.value == 21 and len(p_hand.cards) == 2:
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
p_chips.total += round(p_chips.bet * 1.5)
setLastCmdJack(nodeID, "dealerTurn")
# Save the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cash'] = p_chips.total
break
else:
# Display the statistics
stats = success_rate(next_card, p_hand)
msg += stats
setLastCmdJack(nodeID, "betPlaced")
if getLastCmdJack(nodeID) == "betPlaced":
setLastCmdJack(nodeID, "playing")
msg += "(H)it,(S)tand,(F)orfit,(D)ouble"
# save the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cash'] = p_chips.total
jackTracker[i]['bet'] = p_chips.bet
jackTracker[i]['p_cards'] = p_cards
jackTracker[i]['d_cards'] = d_cards
jackTracker[i]['p_hand'] = p_hand
jackTracker[i]['d_hand'] = d_hand
jackTracker[i]['next_card'] = next_card
return msg
while getLastCmdJack(nodeID) == "playing": # Recall var. from hit and stand function
next_card = hits(cards_deck)
# Get the statistics
stats = success_rate(next_card, p_hand)
# Player's Turn
choice = message.lower()
if choice == "hit" or choice == "h":
# hits(obj_de, p_hand)
p_hand.add_cards(next_card)
msg += show_some(p_hand.cards, d_cards, p_hand)
elif choice == "stand" or choice == "s":
setLastCmdJack(nodeID, "dealerTurn")
elif choice == "forfit" or choice == "f":
p_chips.bet = p_chips.bet / 2
setLastCmdJack(nodeID, "dealerTurn")
p_hand.value += 21
elif choice == "double" or choice == "d":
if p_chips.bet * 2 <= p_chips.total:
p_chips.bet *= 2
next_d_card = hits(cards_deck)
p_hand.add_cards(next_d_card)
setLastCmdJack(nodeID, "dealerTurn")
else:
return "You can't Double Down, dont have enough chips"
else:
return "Invalid Choice"
# Check if player bust
if player_bust(p_hand, p_chips):
d_win += 1
msg += "Player:BUST💥"
setLastCmdJack(nodeID, "dealerTurn")
if getLastCmdJack(nodeID) == "playing":
msg += stats
msg += "[H,S,F,D,L]"
# Save the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
jackTracker[i]['cash'] = p_chips.total
jackTracker[i]['bet'] = p_chips.bet
jackTracker[i]['gameStats']['p_win'] = p_win
jackTracker[i]['gameStats']['d_win'] = d_win
jackTracker[i]['gameStats']['draw'] = draw
jackTracker[i]['p_cards'] = p_cards
jackTracker[i]['d_cards'] = d_cards
jackTracker[i]['p_hand'] = p_hand
jackTracker[i]['d_hand'] = d_hand
break
# Exit player's turn
if getLastCmdJack(nodeID) == "dealerTurn":
break
return msg
if getLastCmdJack(nodeID) == "dealerTurn":
# recall the game state
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
p_chips.total = jackTracker[i]['cash']
p_chips.bet = jackTracker[i]['bet']
p_win = jackTracker[i]['gameStats']['p_win']
d_win = jackTracker[i]['gameStats']['d_win']
draw = jackTracker[i]['gameStats']['draw']
p_cards = jackTracker[i]['p_cards']
d_cards = jackTracker[i]['d_cards']
p_hand = jackTracker[i]['p_hand']
d_hand = jackTracker[i]['d_hand']
next_card = jackTracker[i]['next_card']
break
if p_hand.value <= 21:
# Dealer's Turn
while d_hand.value < 17:
d_card = hits(cards_deck)
d_hand.add_cards(d_card)
if dealer_bust(d_hand, p_hand, p_chips):
p_win += 1
msg += "Dealer:BUST💥"
break
# Show all cards
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
# Check who wins
if push(p_hand, d_hand):
draw += 1
msg += f"👌PUSH"
elif player_wins(p_hand, d_hand, p_chips):
p_win += 1
msg += f"🎉PLAYER WINS🎰"
elif dealer_wins(p_hand, d_hand, p_chips):
d_win += 1
msg += f"👎DEALER WINS"
else:
msg += f"👎DEALER WINS"
# Display the Game Stats
msg += gameStats(str(p_win), str(d_win), str(draw))
# Display the chips left
if p_chips.total < 1:
if p_chips.total > 0:
msg += f"🪙Keep the change you filthy animal!"
else:
msg += "💸NO MORE MONEY! Game Over!"
p_chips.total = jack_starting_cash
else:
msg += f"💰You have {p_chips.total} chips left"
msg += "(B)et or (L)eave table."
# Reset the game
setLastCmdJack(nodeID, "new")
jackTracker[i]['cash'] = p_chips.total
jackTracker[i]['gameStats']['p_win'] = p_win
jackTracker[i]['gameStats']['d_win'] = d_win
jackTracker[i]['gameStats']['draw'] = draw
jackTracker[i]['p_cards'] = []
jackTracker[i]['d_cards'] = []
jackTracker[i]['p_hand'] = []
jackTracker[i]['d_hand'] = []
jackTracker[i]['time'] = time.time()
return msg

652
modules/dopewar.py Normal file
View File

@@ -0,0 +1,652 @@
# Port of https://github.com/Reconfirefly/drugwars/tree/master
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
import pickle
from modules.log import *
# Global variables
total_days = 7 # number of days or rotations the player has to play
starting_cash = 5000
# Database for the game reset on boot
dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount': []}]
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
# high score is saved in a pickle file
dwHighScore = {}
class Drugs:
def __init__(self, name, price_range):
self.name = name
self.price_range = price_range
self.price_check()
def price_check(self):
# the * is to unpack the touple of values that the random goes between
self.price = random.randint(*self.price_range)
# print("the price of " + self.name + " is " + str(self.price))
return self.price
class Events:
def __init__(self, name, text, price_range):
self.name = name
self.price_range = price_range
self.text = text
self.price_mod()
def price_mod(self):
self.price = random.randint(*self.price_range)
return self.price
my_drugs = [
# Drugs("Name", (min price, max price), amount)
Drugs("Cocaine", (15000, 28000)),
Drugs("Heroin", (2000, 10000)),
Drugs("Weed", (300, 1000)),
Drugs("Hash", (200, 1200)),
Drugs("Opium", (400, 1800)),
Drugs("Acid", (1000, 4200)),
Drugs("Ludes", (18, 75)),
]
event_list = [
# Events("Name", "Text", (min price, max price))
Events("Cocaine", 'El Chapo Arrested! 🚔 Coke price thru the roof! 📈', (40000, 110000)),
Events("Heroin", 'Trump cracks down on opiates! Heroin in high demand by addicts📈', (9000, 25000)),
Events("Weed", 'The DEA has fully legalized weed! Prices are at an all time low!📉', (50, 400)),
Events("Hash", 'Ricky\'s hash driveway burned down! 🚒 Look at the price boys!📈', (800, 2000)),
Events("Opium", 'Shenzhen 深圳 Opium 鸦片 Den 塔 was raided! 🚔 Street price is popping off!📈', (1800, 6000)),
Events("Acid", 'The Grateful Dead are on tour! Acid prices are skyrocketing!📈', (5000, 15000)),
Events("Ludes", 'The Wolf of Wall Street is back! Ludes are in demand!', (100, 300)),
Events("Cocaine", "The Biden administration has legalized cocaine! Prices are at an all time low!📉", (3000, 10000)),
Events("Heroin", "Oregon has legalized heroin! Prices are at an all time low!📉", (500, 2500)),
Events("Weed", "Prices are at an all time HIGH!📈", (1000, 5000)),
Events("Hash", "The Middle East has legalised hash! Prices are at an all time low!📉", (50, 1000)),
Events("Opium", "The Sackler's flood the market with cheap opium! Prices are at an all time low!📉", (300, 900)),
Events("Acid", "The FBI admits to dosing the water supply with LSD! Acid at an all time low!📉", (500, 2000)),
Events("Ludes", "The FDA approves ludes for sale! Prices are at an all time low!📉", (3, 45))
]
def generatelocations():
# dictionary of locations
locs = {'Canada': ('Red Deer', 'Edmonton', 'Calgary', 'Toronto', 'Vancouver', 'St. Johns'),
'USA': ('L.A.', 'NYC', 'Chicago', 'Miami', 'Houston', 'Phoenix'), 'Mexico': ('Tijuana', 'Mexico City', 'Cancun', 'Juarez', 'Acapulco', 'Guadalajara'),\
'South America': ('Bogota', 'Caracas', 'Lima', 'Santiago', 'Buenos Aires', 'Rio'), 'Europe': ('London', 'Paris', 'Berlin', 'Rome', 'Madrid', 'Moscow')}
country = list(locs.keys())
country = country[random.randint(0, len(country)-1)]
# return the location list for the user's country
location = []
for i in range(len(locs[country])):
location.append(locs[country][i])
return location
def generate_event():
# roll to see if an event happens
event_choice = random.randint(0, len(event_list)-1)
if random.randint(0, 100) > 35:
return event_choice
else:
return -1
def officer(nodeID):
global dwCashDb, dwInventoryDb
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
inventory = dwInventoryDb[i].get('inventory')
amount = check_inv(nodeID)
# get the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == 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
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
cash_taken = 'conf'
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
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
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
def price_change(event_number):
price_list = []
for i in range(0, len(my_drugs)):
j = my_drugs[i]
k = j.price_check()
price_list.append(k)
# check if IndexError will be thrown and find a new event_number with generate_event
while event_number > len(price_list)-1:
event_number = generate_event()
if event_number != -1:
price_list[event_number] = event_list[event_number].price_mod()
return price_list
def check_inv(nodeID):
global dwInventoryDb
# get the inventory ammount for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
amount = dwInventoryDb[i].get('amount')
# if ammount is empty list initialize it
if not amount:
amount = []
for i in range(0, len(my_drugs)):
amount.append(0,)
# save the amount to the inventory_db
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['amount'] = amount
return amount
def buy_func(nodeID, price_list, choice=0, value='0'):
global dwCashDb, dwInventoryDb, dwPlayerTracker
msg = ''
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
inventory = dwInventoryDb[i].get('inventory')
amount = check_inv(nodeID)
# get the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
cash = dwCashDb[i].get('cash')
drug_choice = choice
if choice == 0:
msg = f"Didnt see a drug chouce. ex: s,1,10 sells 10 of drug 1{my_drugs[1].name}, or p for price list"
return msg
else:
if drug_choice in range(1, len(my_drugs) + 1):
drug_choice = drug_choice - 1
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
msg += " The going price is: $" + str(price_list[drug_choice]) + " "
buy_amount = value
if buy_amount == 'm':
buy_amount = cash // price_list[drug_choice]
if buy_amount > 100 - inventory:
buy_amount = 100 - inventory
# set the buy amount to the max if the user enters m
buy_amount = int(buy_amount)
if buy_amount == 0:
msg = f"Didnt see a qty. ex: b,1,10 buys 10 of {my_drugs[1].name}, can also use m for max"
return msg
elif buy_amount not in range(1, 101):
msg = f"Enter qty or m for max"
return msg
elif buy_amount > 100 - inventory:
msg = "You don\'t have enough space for all that.🎒"
return msg
elif buy_amount * price_list[drug_choice] <= cash:
amount[drug_choice] += buy_amount
cash -= buy_amount * price_list[drug_choice]
inventory += buy_amount
msg += "You bought " + str(buy_amount) + " " + my_drugs[drug_choice].name + '. Remaining cash: $' + str(cash)
msg += f"\nBuy Sell Fly?"
else:
msg = "You don't have enough cash!😭"
return msg
# update the cash_db and inventory_db values
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb[i]['cash'] = cash
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
# save the last command as ask_bsf
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
return msg
def sell_func(nodeID, price_list, choice=0, value='0'):
global dwCashDb, dwInventoryDb, dwPlayerTracker
msg = ''
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
inventory = dwInventoryDb[i].get('inventory')
amount = check_inv(nodeID)
# get the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
cash = dwCashDb[i].get('cash')
# get the drug choice and amount to sell
drug_choice = choice
sell_amount = value
try:
if sell_amount == 'm':
sell_amount = amount[drug_choice - 1]
else:
sell_amount = int(sell_amount)
if sell_amount not in range(1, 101):
msg = f"You can only sell between 1 and 100"
return msg
except ValueError:
msg = f"Enter qty or m for max"
return msg
# check if the user has any of the drug they are trying to sell
if choice == 0:
msg = "Enter b or s and the drug number and qty you want to buy or sell. ex: b,1,10 buys 10 of drug 1"
return msg
else:
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
drug_choice = drug_choice - 1
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
" The going price is: $" + str(price_list[drug_choice])
# check if the user has enough of the drug to sell
if sell_amount <= amount[drug_choice]:
amount[drug_choice] -= sell_amount
cash += sell_amount * price_list[drug_choice]
inventory -= sell_amount
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name + ' for $' +\
str(sell_amount * price_list[drug_choice]) + '. Total cash: $' + str(cash)
else:
msg = "You don't have that much"
return msg
else:
msg = "You don't have any " + my_drugs[drug_choice - 1].name
return msg
# update the cash_db and inventory_db values
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb[i]['cash'] = cash
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
dwInventoryDb[i]['amount'] = amount
# save the last command as ask_bsf
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
return msg
def get_location_table(nodeID, choice=0):
global dwLocationDb
# get the location for the user
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
loc = dwLocationDb[i].get('location')
# list the lcaitons and their index in two columns
loc_table_string = ''
for i in range(len(loc)):
loc_table_string += str(i+1) + '. ' + loc[i] + ' '
loc_table_string += f' Where do you want to 🛫?#'
return loc_table_string
def endGameDw(nodeID):
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
msg = ''
dwHighScore = getHighScoreDw()
# Confirm the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
cash = dwCashDb[i].get('cash')
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
# remove the player from the game databases
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb.pop(i)
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb.pop(i)
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
dwLocationDb.pop(i)
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
dwGameDayDb.pop(i)
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker.pop(i)
# checks if the player's score is higher than the high score and writes a new high score if it is
if cash > dwHighScore.get('cash'):
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
with open('dopewar_hs.pkl', 'wb') as file:
pickle.dump(dwHighScore, file)
msg = "You finished with $" + str(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.'
return msg
if cash == starting_cash:
msg = 'You broke even... hope you at least had fun 💉💊'
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
def getHighScoreDw():
global dwHighScore
# Load high score table
try:
with open('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:
pickle.dump(dwHighScore, file)
return dwHighScore
def render_game_screen(userID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen):
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"
# get the inventory for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == userID:
inventory = dwInventoryDb[i].get('inventory')
amount = check_inv(userID)
# get the cash for the user
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == userID:
cash = dwCashDb[i].get('cash')
msg += "Location: " + loc[int(loc_choice) - 1] + ", Day:" + str(day_play) + '/' + str(total_day) + " 🎒: " + str(inventory) + "/100" + ", $" + str(cash) + f"\n"
for i, drug in enumerate(my_drugs, 1):
qty = amount[i-1]
msg += f'#{str(i)}.{drug.name}/${price_list[i-1]}({qty}) '
return msg
def dopeWarGameDay(nodeID, day_play, total_day):
global dwCashDb, dwLocationDb, dwInventoryDb
cash_stolen = 0
# roll for the event of the day
event_number = generate_event()
# get the location for the user
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
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)
price_list = price_change(event_number)
# set the price_list for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['priceList'] = price_list
check_inv(nodeID)
# main game display print
msg = render_game_screen(nodeID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen)
return msg
def playDopeWars(nodeID, cmd):
global dwGameDayDb, dwPlayerTracker, dwCashDb, dwInventoryDb, dwLocationDb, dwHighScore
inGame = False
msg = ''
# check if the player is currently playing the game
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
inGame = True
if not inGame:
# initalize player in the database
loc = generatelocations()
dwInventoryDb.append({'userID': nodeID, 'inventory': 0, 'priceList': []})
dwCashDb.append({'userID': nodeID, 'cash': starting_cash})
dwLocationDb.append({'userID': nodeID, 'location': loc, 'loc_choice': 0})
dwGameDayDb.append({'userID': nodeID, 'day': 0})
dwPlayerTracker.append({'userID': nodeID, 'last_played': time.time(), 'cmd': 'start'})
logger.debug("System: DopeWars: New player: " + str(nodeID))
# get the day for the user
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
game_day = dwGameDayDb[i].get('day')
# get the player's last command
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
last_cmd = dwPlayerTracker[i].get('cmd')
# get the price_list for the user
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
price_list = dwInventoryDb[i].get('priceList')
# get the location for the user
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
loc_choice = dwLocationDb[i].get('loc_choice')
# Pick Starting City
if last_cmd == 'start':
# print the location table
msg = get_location_table(nodeID)
# set the player's last command to location to start the game
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'location'
if last_cmd == 'ask_bsf':
msg = 'example Buy: b,Drug,Qty or Sell s,1,10. Fly: f. Price list: p or end'
menu_choice = cmd.lower()
if ',' in menu_choice or '.' in menu_choice:
#split the choice into a letter and a number for the buy/sell functions
try:
if '.' in menu_choice:
menu_choice = menu_choice.split('.')
if ',' in menu_choice:
menu_choice = menu_choice.split(',')
if int(menu_choice[1]) not in range(1, 8):
raise ValueError
else:
menu_choice[1] = int(menu_choice[1])
if menu_choice[0] not in ['b', 's']:
raise ValueError
if menu_choice[2] != 'm':
if int(menu_choice[2]) not in range(1, 101):
raise ValueError
else:
menu_choice[2] = int(menu_choice[2])
except ValueError:
msg = 'a value was bad, example dopeware Buy or Sell b,1,10 or s,1,m'
return msg
if menu_choice[0] == 'b':
# set last command to ask_bsf and buy
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
msg = buy_func(nodeID, price_list, menu_choice[1], menu_choice[2])
return msg
if menu_choice[0] == 's':
# set last command to ask_bsf and sell
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
msg = sell_func(nodeID, price_list, menu_choice[1], menu_choice[2])
return msg
elif 's' in menu_choice:
msg = ''
# sell everything we have in backpack
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
inventory = dwInventoryDb[i].get('inventory')
if inventory == 0:
msg = "You don't have anything to sell🚭"
else:
for i in range(1, (len(my_drugs) +1)):
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'
return msg
elif 'f' in menu_choice:
# set last command to location
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'location'
last_cmd = 'location'
elif 'p' in menu_choice:
# render_game_screen
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0)
return msg
elif 'end' in menu_choice:
msg = endGameDw(nodeID)
return msg
else:
msg = 'example Buy: b,Drug,Qty or Sell s,1,10. Fly: f. Price list: p or end'
return msg
# Buy
if last_cmd == 'buy':
# ned to collect which drug # and qty to buy
msg = buy_func(nodeID, price_list)
return msg
# Sell
if last_cmd == 'sell':
msg = sell_func(nodeID, price_list)
return msg
# Pick Location, and display main game screen
if last_cmd == 'location':
# validate the location choice
try:
loc_choice = int(cmd)
if loc_choice not in range(1, 6):
raise ValueError
except ValueError:
loc_choice = random.randint(1, 6)
# set the player's location choice
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
dwLocationDb[i]['loc_choice'] = loc_choice
# set the player's last command
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'display_main'
# increment the game_day
game_day += 1
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
dwGameDayDb[i]['day'] = game_day
# update the player's last played time
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['last_played'] = time.time()
last_cmd = 'display_main'
# Display Main Game Screen and ask for buy, sell, or fly
if last_cmd == 'display_main':
msg = dopeWarGameDay(nodeID, game_day, total_days)
msg += f"\nBuy, Sell, Fly? Price list?"
# set the player's last command
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
# Game end
if game_day == total_days + 1:
msg = endGameDw(nodeID)
return msg

571
modules/lemonade.py Normal file
View File

@@ -0,0 +1,571 @@
# Port of https://github.com/tigerpointe/Lemonade-Stand/blob/main/lemonade.py MIT License Copyright (c) 2023 TigerPointe Software, LLC
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
from collections import OrderedDict # ordered dictionaries
from random import randrange, uniform # random numbers
from types import SimpleNamespace # namespaces support
import pickle # pickle file support
import time # time functions
from modules.log import * # mesh-bot logging
import locale # culture specific locale
import math # math functions
import re # regular expressions
# Set all of the locale category elements as default
# ex. print(locale.currency(12345.67, grouping=True))
locale.setlocale(locale.LC_ALL, '')
lemon_starting_cash = 30.00
lemon_total_weeks = 7
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'time': time.time()}]
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00}]
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
def get_sales_amount(potential, unit, price):
"""Gets the sales amount.
Multiply the potential sales by a ratio of unit cost to actual price; the
exponent results in the values falling along a curve, rather than along a
straight line, resulting in more realistic sales values at each price.
Parameters
potential : Potential sales
unit : Unit cost
price : Actual price
"""
return math.floor(potential * (unit / (price ** 1.5)))
def getHighScoreLemon():
global high_score
# Load high score table
try:
with open('lemonade_hs.pkl', 'rb') as file:
high_score = pickle.load(file)
except FileNotFoundError:
logger.debug("System: Lemonade: No high score table found")
# high score pickle file is a touple of the nodeID and the high score
high_score = ({"userID": 4258675309, "cash": 2, "success": 0})
# write a new high score file if one is not found
with open('lemonade_hs.pkl', 'wb') as file:
pickle.dump(high_score, file)
return high_score
def start_lemonade(nodeID, message, celsius=False):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
potential = 0
unit = 0.0
price = 0.0
high_score = getHighScoreLemon()
def saveValues():
# save playerDB values
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cups'] = inventory.cups
lemonadeTracker[i]['lemons'] = inventory.lemons
lemonadeTracker[i]['sugar'] = inventory.sugar
lemonadeTracker[i]['cash'] = inventory.cash
lemonadeTracker[i]['start'] = inventory.start
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
lemonadeCups[i]['cost'] = cups.cost
lemonadeCups[i]['unit'] = cups.unit
for i in range(len(lemonadeLemons)):
if lemonadeLemons[i]['nodeID'] == nodeID:
lemonadeLemons[i]['cost'] = lemons.cost
lemonadeLemons[i]['unit'] = lemons.unit
for i in range(len(lemonadeSugar)):
if lemonadeSugar[i]['nodeID'] == nodeID:
lemonadeSugar[i]['cost'] = sugar.cost
lemonadeSugar[i]['unit'] = sugar.unit
for i in range(len(lemonadeWeeks)):
if lemonadeWeeks[i]['nodeID'] == nodeID:
lemonadeWeeks[i]['current'] = weeks.current
lemonadeWeeks[i]['total'] = weeks.total
lemonadeWeeks[i]['sales'] = weeks.sales
lemonadeWeeks[i]['potential'] = potential
lemonadeWeeks[i]['unit'] = unit
lemonadeWeeks[i]['price'] = price
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
lemonadeScore[i]['value'] = score.value
lemonadeScore[i]['total'] = score.total
def endGame(nodeID):
# remove the player from the tracker
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker.pop(i)
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
lemonadeCups.pop(i)
for i in range(len(lemonadeLemons)):
if lemonadeLemons[i]['nodeID'] == nodeID:
lemonadeLemons.pop(i)
for i in range(len(lemonadeSugar)):
if lemonadeSugar[i]['nodeID'] == nodeID:
lemonadeSugar.pop(i)
for i in range(len(lemonadeWeeks)):
if lemonadeWeeks[i]['nodeID'] == nodeID:
lemonadeWeeks.pop(i)
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
lemonadeScore.pop(i)
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
# Check for end of game
if "end" in message.lower():
endGame(nodeID)
return "Goodbye!👋"
title="Lemonade Stand🍋"
# Define the temperature unit symbols
fahrenheit_unit = "ºF"
celsius_unit = "ºC"
# Inventory data (contains the item levels)
inventoryd = {
'cups' : 0,
'lemons' : 0,
'sugar' : 0,
'cash' : lemon_starting_cash,
'start' : lemon_starting_cash
}
inventory = SimpleNamespace(**inventoryd)
# Cups data (includes a calculated cost per unit)
cupsd = {
'cost' : 2.50, # current price
'count' : 25, # servings per box
'min' : 0.99, # minimum price
'unit' : 0.00 # unit price
}
cups = SimpleNamespace(**cupsd)
cups.unit = round(cups.cost / cups.count, 2)
# Lemons data (includes a calculated cost per unit)
lemonsd = {
'cost' : 4.00, # current price
'count' : 8, # servings per bag
'min' : 2.00, # minimum price
'unit' : 0.00 # unit price
}
lemons = SimpleNamespace(**lemonsd)
lemons.unit = round(lemons.cost / lemons.count, 2)
# Sugar data (includes a calculated cost per unit)
sugard = {
'cost' : 3.00, # current price
'count' : 15, # servings per bag
'min' : 1.50, # minimum price
'unit' : 0.00 # unit price
}
sugar = SimpleNamespace(**sugard)
sugar.unit = round(sugar.cost / sugar.count, 2)
# Weeks data (measures the session duration)
weeksd = {
'current' : 1, # start with the 1st week
'total' : 12, # span the 12 weeks of Summer
'sales' : 99, # 99 maximum sales per week
'summary' : [] # empty array
}
weeks = SimpleNamespace(**weeksd)
# Forecast data (includes percentage values, UTF8 glyphs and display names)
forecastd = OrderedDict()
forecastd['sunny'] = [1.00, 0x2600, "Sunny"]
forecastd['partly'] = [0.90, 0x26C5, "Partly Sunny"]
forecastd['cloudy'] = [0.70, 0x2601, "Mostly Cloudy"]
forecastd['rainy'] = [0.40, 0x2602, "Rainy"]
forecastd['stormy'] = [0.10, 0x26C8, "Stormy"]
# Temperature data (uses Fahrenheit as the percentage values)
temperatured = {
'min' : 69,
'max' : 100,
'units' : fahrenheit_unit,
'forecast' : None,
'value' : None
}
temperature = SimpleNamespace(**temperatured)
# Score data (based on actual vs. maximum net sales)
scored = {
'value' : 0.00,
'total' : 0.00
}
score = SimpleNamespace(**scored)
# Check for Celsius
if (celsius):
temperature.units = celsius_unit
# load playerDB values
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
inventory.cups = lemonadeTracker[i]['cups']
inventory.lemons = lemonadeTracker[i]['lemons']
inventory.sugar = lemonadeTracker[i]['sugar']
inventory.cash = lemonadeTracker[i]['cash']
inventory.start = lemonadeTracker[i]['start']
last_cmd = lemonadeTracker[i]['cmd']
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
cups.cost = lemonadeCups[i]['cost']
cups.unit = lemonadeCups[i]['unit']
for i in range(len(lemonadeLemons)):
if lemonadeLemons[i]['nodeID'] == nodeID:
lemons.cost = lemonadeLemons[i]['cost']
lemons.unit = lemonadeLemons[i]['unit']
for i in range(len(lemonadeSugar)):
if lemonadeSugar[i]['nodeID'] == nodeID:
sugar.cost = lemonadeSugar[i]['cost']
sugar.unit = lemonadeSugar[i]['unit']
for i in range(len(lemonadeWeeks)):
if lemonadeWeeks[i]['nodeID'] == nodeID:
weeks.current = lemonadeWeeks[i]['current']
weeks.total = lemonadeWeeks[i]['total']
weeks.sales = lemonadeWeeks[i]['sales']
potential = lemonadeWeeks[i]['potential']
unit = lemonadeWeeks[i]['unit']
price = lemonadeWeeks[i]['price']
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):
if "new" in last_cmd:
# set the last command to cups in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
# Create a new display buffer for the text messages
buffer= ""
# the current week number
buffer += title + "Week #" + str(weeks.current) + "of" + str(weeks.total)
# Generate a random weather forecast and temperature and display
temperature.forecast = randrange(0, len(forecastd))
temperature.value = randrange(temperature.min, temperature.max)
formatted = str(temperature.value)
if (temperature.units == celsius_unit):
formatted = str(round(((temperature.value - 32) * (5/9))))
glyph = chr(forecastd[list(forecastd)[temperature.forecast]][1])
buffer += ". " + \
formatted + temperature.units + " " + \
forecastd[list(forecastd)[temperature.forecast]][2] + \
" " + glyph
# Calculate the potential sales as a percentage of the maximum value
# (lower temperature = fewer sales, severe weather = fewer sales)
forecast = forecastd[list(forecastd)[temperature.forecast]][0]
potential = math.floor(weeks.sales * \
(temperature.value / 100) * \
forecast)
# Update the cups cost
cups.cost = cups.cost + round(uniform(-1.50, 1.50), 2)
if (cups.cost < cups.min):
cups.cost = cups.min
cups.unit = round(cups.cost / cups.count, 2)
# Update the lemons cost
lemons.cost = lemons.cost + round(uniform(-1.50, 1.50), 2)
if (lemons.cost < lemons.min):
lemons.cost = lemons.min
lemons.unit = round(lemons.cost / lemons.count, 2)
# Update the sugar cost
sugar.cost = sugar.cost + round(uniform(-1.50, 1.50), 2)
if (sugar.cost < sugar.min):
sugar.cost = sugar.min
sugar.unit = round(sugar.cost / sugar.count, 2)
# Calculate the unit cost and display the estimated sales from the forecast potential
unit = cups.unit + lemons.unit + sugar.unit
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
buffer += " Sales Potential:" + str(potential) + " cups."
# Display the current inventory
buffer += " Inventory:"
buffer += "🥤:" + str(inventory.cups)
buffer += "🍋:" + str(inventory.lemons)
buffer += "🍚:" + str(inventory.sugar)
# Display the updated item prices
buffer += f"\nPrices: "
buffer += "🥤:" + \
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + \
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + \
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
# Display the current cash
gainloss = inventory.cash - inventory.start
buffer += " 💵:" + \
locale.currency(inventory.cash, grouping=True)
# if the player is in the red
pnl = locale.currency(gainloss, grouping=True)
if "0.00" not in pnl:
if pnl.startswith("-"):
buffer += "📊P&L📉" + pnl
else:
buffer += "📊P&L📈" + pnl
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
saveValues()
return buffer
if "cups" in last_cmd:
# Read the number of cup boxes to purchase
newcups = -1
if "n" in message.lower():
message = "0"
try:
newcups = int(message)
if (newcups > 0):
cost = round(newcups * cups.cost, 2)
if (cost > inventory.cash):
return "You do not have enough 💵."
inventory.cups += (newcups * cups.count)
inventory.cash -= cost
msg = "Purchased " + str(newcups) + " 📦 "
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
else:
msg = "No 🥤 were purchased"
except Exception as e:
return "invalid input, enter the number of 🥤 to purchase or (N)one"
# set the last command to lemons in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "lemons"
saveValues()
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
return msg
if "lemons" in last_cmd:
# Read the number of lemon bags to purchase
newlemons = -1
if "n" in message.lower():
message = "0"
try:
newlemons = int(message)
if (newlemons > 0):
cost = round(newlemons * lemons.cost, 2)
if (cost > inventory.cash):
return "You do not have enough cash."
inventory.lemons += (newlemons * lemons.count)
inventory.cash -= cost
msg = "Purchased " + str(newlemons) + " 🧺 "
msg += str(inventory.lemons) + " 🍋 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
else:
msg = "No 🍋 were purchased"
except Exception as e:
newlemons = -1
return "invalid input, enter the number of 🍋 to purchase"
# set the last command to sugar in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sugar"
saveValues()
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
return msg
if "sugar" in last_cmd:
# Read the number of sugar bags to purchase
newsugar = -1
if "n" in message.lower():
message = "0"
try:
newsugar = int(message)
if (newsugar > 0):
cost = round(newsugar * sugar.cost, 2)
if (cost > inventory.cash):
return "You do not have enough cash."
inventory.sugar += (newsugar * sugar.count)
inventory.cash -= cost
msg = " Purchased " + str(newsugar) + " bag(s) of 🍚 for " + locale.currency(cost, grouping=True)
msg += ". " + str(inventory.sugar) + f"🥤🍚 in inventory."
else:
msg = "No additional 🍚 was purchased"
except Exception as e:
return "invalid input, enter the number of 🍚 bags to purchase"
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
# set the last command to price in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "price"
saveValues()
return msg
if "price" in last_cmd:
# set the last command to sales in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sales"
if "g" in message.lower():
lemonadeTracker[i]['cmd'] = "cups"
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
return msg
else:
last_cmd = "sales"
# Read the actual price
price = 0.00
while (price <= 0.00):
try:
raw = message
price = float(re.sub("[^0-9.-]", "", raw) or 0.00)
if (price <= 0.00):
return "The price must be greater than zero."
except Exception as e:
price = 0.00
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()
if "sales" in last_cmd:
# Calculate the weekly sales based on price and lowest inventory level
# (higher markup price = fewer sales, limited by the inventory on-hand)
sales = get_sales_amount(potential, unit, price)
sales = min(potential, sales, \
inventory.cups, inventory.lemons, \
inventory.sugar) # "min" returns lowest value
margin = price - unit
gross = sales * price
net = sales * margin
# Add a new row to the summary
weeks.summary.append({ 'sales' : sales, 'price' : price })
# Update the inventory levels to reflect consumption
inventory.cups = inventory.cups - sales
inventory.lemons = inventory.lemons - sales
inventory.sugar = inventory.sugar - sales
inventory.cash = inventory.cash + gross
gainloss= inventory.cash - inventory.start
# Display the calculated sales information
msg = "Results Week📊#" + str(weeks.current) + "of" + str(weeks.total)
msg += " Cost/Price:" + locale.currency(unit, grouping=True) + "/" + locale.currency(price, grouping=True)
msg += " P.Margin:" + locale.currency(margin, grouping=True)
msg += " T.Sales:" + str(sales) + "@" + locale.currency(price, grouping=True)
msg += " G.Profit: " + locale.currency(gross, grouping=True)
msg += " N.Profit:" + locale.currency(net, grouping=True)
# Display the updated inventory levels
msg += "\nRemaining"
msg += " 🥤:" + str(inventory.cups)
msg += " 🍋:" + str(inventory.lemons)
msg += " 🍚:" + str(inventory.sugar)
msg += " 💵:" + locale.currency(inventory.cash, grouping=True)
# Display the gain/loss
pnl = locale.currency(gainloss, grouping=True)
if "0.00" not in pnl:
if pnl.startswith("-"):
msg += "📊P&L📉" + pnl
else:
msg += "📊P&L📈" + pnl
# Display the weekly sales summary
pad_week = len(str(weeks.total))
pad_sale = len(str(weeks.sales))
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']
# Loop through a range of prices to find the highest net profit
maxsales = 0
maxprice = 0.00
maxgross = 0.00
maxnet = 0.00
minnet = net
for i in range(25, 2500, 25):
price = i / 100 # range uses integers, not currency (floats)
sales = get_sales_amount(potential, unit, price)
margin = price - unit
gross = sales * price
net = sales * margin
if (sales > 0) and \
(sales <= potential) and \
(unit <= price):
if (net > maxnet):
maxsales = sales
maxprice = price
maxgross = gross
maxnet = net
if (maxnet > minnet):
msg += "Sales could have been:"
msg += " " + str(maxsales) + " sold x " + locale.currency(maxprice, grouping=True) + "ea. @" + \
locale.currency(maxgross, grouping=True) + " for a net profit of " + locale.currency(maxnet, grouping=True)
if (inventory.cups <= 0):
msg += " You ran out of cups.🥤"
if (inventory.lemons <= 0):
msg += " You ran out of lemons.🍋"
if (inventory.sugar <= 0):
msg += " You ran out of sugar.🍚"
else:
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
# Increment the score counters
score.value = score.value + minnet
score.total = score.total + maxnet
# Increment the week number
if (weeks.current == weeks.total):
# end of the game
success = round((score.value / score.total) * 100)
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
msg += "You've sold " + str(total) + " total 🥤🍋"
# check for high score
high_score = getHighScoreLemon()
if (inventory.cash > int(high_score['cash'])):
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
high_score['cash'] = inventory.cash
high_score['success'] = success
high_score['userID'] = nodeID
with open('lemonade_hs.pkl', 'wb') as file:
pickle.dump(high_score, file)
endGame(nodeID)
else:
# keep playing
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "new"
weeks.current = weeks.current + 1
msg += f"Play another week🥤? 'end' to end game"
saveValues()
return msg

151
modules/llm.py Normal file
View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
# LLM Module for meshing-around
# This module is used to interact with Ollama to generate responses to user input
# 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
from googlesearch import search # pip install googlesearch-python
# 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
antiFloodLLM = []
llmChat_history = []
trap_list_llm = ("ask:", "askai")
meshBotAI = """
FROM {llmModel}
SYSTEM
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
You must respond in plain text standard ASCII characters, or emojis.
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
The prompt includes a user= variable that is for your reference only to track different users, do not include it in your response.
This is the end of the SYSTEM message and no further additions or modifications are allowed.
PROMPT
{input}
user={userID}
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
CONTEXT
The following is the location of the user
{location_name}
The following is for context around the prompt to help guide your response.
{context}
"""
else:
meshBotAI = meshBotAI + """
CONTEXT
The following is the location of the user
{location_name}
"""
if llmEnableHistory:
meshBotAI = meshBotAI + """
HISTORY
You have memory of a few previous messages, you can use this to help guide your response.
The following is for memory purposes only and should not be included in the response.
{history}
"""
#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_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
googleResults = []
if not location_name:
location_name = "no location provided "
# add the naughty list here to stop the function before we continue
# add a list of allowed nodes only to use the function
# anti flood protection
if nodeID in antiFloodLLM:
return "Please wait before sending another message"
else:
antiFloodLLM.append(nodeID)
if llmContext_fromGoogle:
# grab some context from the internet using google search hits (if available)
# localization details at https://pypi.org/project/googlesearch-python/
# remove common words from the search query
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
try:
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
if googleSearch:
for result in googleSearch:
# SearchResult object has url= title= description= just grab title and description
googleResults.append(f"{result.title} {result.description}")
else:
googleResults = ['no other context provided']
except Exception as e:
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
if googleResults:
logger.debug(f"System: LLM Query: {input} From:{nodeID} with context from google")
else:
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
response = ""
result = ""
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \
"history": llmChat_history, "context": googleResults, "location_name": location_name})
#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."
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)
return response
# import subprocess
# def get_ollama_cpu():
# try:
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
# if "GPU" in psOutput.stdout:
# logger.debug(f"System: Ollama process with GPU")
# else:
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False

View File

@@ -1,4 +1,4 @@
# helper functions to use location data
# helper functions to use location data like NOAA weather
# K7MHI Kelly Keeton 2024
import json # pip install json
@@ -7,40 +7,53 @@ import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.settings import *
from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
def where_am_i(lat=0, lon=0):
def where_am_i(lat=0, lon=0, short=False):
whereIam = ""
grid = mh.to_maiden(float(lat), float(lon))
if float(lat) == 0 and float(lon) == 0:
if int(float(lat)) == 0 and int(float(lon)) == 0:
logger.error("Location: No GPS data, try sending location")
return NO_DATA_NOGPS
# initialize Nominatim API
geolocator = Nominatim(user_agent="mesh-bot")
# Nomatim API call to get address
if float(lat) == latitudeValue and float(lon) == longitudeValue:
# redacted address when no GPS and using default location
location = geolocator.reverse(lat + ", " + 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
return whereIam
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
try:
# Nomatim API call to get address
if short:
location = geolocator.reverse(lat + ", " + lon)
address = location.raw['address']
address_components = ['city', 'state', 'county', 'country']
whereIam = f"City: {address.get('city', '')}. State: {address.get('state', '')}. County: {address.get('county', '')}. Country: {address.get('country', '')}."
return whereIam
if float(lat) == latitudeValue and float(lon) == longitudeValue:
# redacted address when no GPS and using default location
location = geolocator.reverse(lat + ", " + 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
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
return whereIam
except Exception as e:
logger.debug("Location:Error fetching location data with whereami, likely network error")
return ERROR_FETCHING_DATA
def get_tide(lat=0, lon=0):
station_id = ""
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, try sending location for tide")
return NO_DATA_NOGPS
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
try:
@@ -48,14 +61,17 @@ def get_tide(lat=0, lon=0):
if station_data.ok:
station_json = station_data.json()
else:
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA
station_id = station_json['stationList'][0]['stationId']
except (requests.exceptions.RequestException, json.JSONDecodeError):
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
@@ -65,8 +81,10 @@ def get_tide(lat=0, lon=0):
try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok:
logger.error("Location:Error fetching station 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"
@@ -97,7 +115,10 @@ def get_weather(lat=0, lon=0, unit=0):
if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS
# get weather data from NOAA units for metric
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
unit = 1
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
if unit == 1:
weather_url += "&unit=1"
@@ -105,14 +126,17 @@ def get_weather(lat=0, lon=0, unit=0):
try:
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
if not weather_data.ok:
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
table = soup.find('div', id="detailed-forecast-body")
if table is None:
logger.error("Location:Bad weather data from NOAA")
return ERROR_FETCHING_DATA
else:
# get rows
@@ -179,7 +203,9 @@ def abbreviate_weather(row):
"West": "W",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms"
"thunderstorms": "t-storms",
"quarters": "qtrs",
"quarter": "qtr"
}
line = row
@@ -196,12 +222,15 @@ def getWeatherAlerts(lat=0, lon=0):
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"
#logger.debug("Location:Fetching weather alerts from NOAA for " + str(lat) + ", " + str(lon))
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
alerts = ""
@@ -233,16 +262,20 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
if float(lat) == 0 and float(lon) == 0:
logger.warning("Location:No GPS data, try sending location for weather alerts")
return NO_DATA_NOGPS
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"
#logger.debug("Location:Fetching weather alerts detailed from NOAA for " + str(lat) + ", " + str(lon))
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
alerts = ""

View File

@@ -1,3 +1,7 @@
# Custom logger for MeshBot and PongBot
# you can change the sdtout_handler level to logging.INFO to only show INFO level logs
# stdout_handler.setLevel(logging.INFO)vs stdout_handler.setLevel(logging.DEBUG)
# 2024 Kelly Keeton K7MHI
import logging
from datetime import datetime
from modules.settings import *
@@ -42,6 +46,7 @@ msgLogger.propagate = False
# Define format for logs
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
msgLogFormat = '%(asctime)s | %(message)s'
today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
@@ -50,13 +55,18 @@ stdout_handler.setLevel(logging.DEBUG)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Create file handler for logging to a file
today = datetime.now()
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(msgLogFormat))
# Add handlers to the logger
logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('system{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler.setFormatter(logging.Formatter(logFormat))
logger.addHandler(file_handler)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)

View File

@@ -5,7 +5,7 @@
import socket
import asyncio
from modules.settings import *
from modules.log import *
def get_hamlib(msg="f"):
try:
@@ -13,7 +13,7 @@ def get_hamlib(msg="f"):
rigControlSocket.settimeout(2)
rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1])))
except Exception as e:
print(f"\nSystem: Error connecting to rigctld: {e}")
logger.error(f"RadioMon: Error connecting to rigctld: {e}")
return ERROR_FETCHING_DATA
try:
@@ -27,7 +27,7 @@ def get_hamlib(msg="f"):
data = data.replace(b'\n',b'')
return data.decode("utf-8").rstrip()
except Exception as e:
print(f"\nSystem: Error fetching data from rigctld: {e}")
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
return ERROR_FETCHING_DATA
def get_freq_common_name(freq):
@@ -140,6 +140,7 @@ async def signalWatcher():
signalStrength = int(get_sig_strength())
if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold:
message = f"Detected {get_freq_common_name(get_hamlib('f'))} active. S-Meter:{signalStrength}dBm"
logger.debug(f"RadioMon: {message}. Waiting for {signalHoldTime} seconds")
previousStrength = signalStrength
signalCycle = 0
await asyncio.sleep(signalHoldTime)

View File

@@ -1,3 +1,5 @@
# Settings for MeshBot and PongBot
# 2024 Kelly Keeton K7MHI
import configparser
# messages
@@ -8,7 +10,6 @@ MOTD = 'Thanks for using MeshBOT! Have a good day!'
NO_ALERTS = "No weather alerts found."
# setup the global variables
MESSAGE_CHUNK_SIZE = 160 # message chunk size for sending at high success rate
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
msg_history = [] # message history for the store and forward feature
bbs_ban_list = [] # list of banned users, imported from config
@@ -22,6 +23,10 @@ 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
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -40,6 +45,34 @@ if config.sections() == []:
config.write(open(config_file, 'w'))
print (f"System: Config file created, check {config_file} or review the config.template")
if 'sentry' not in config:
config['Sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
config['bbs'] = {'enabled': 'False', 'bbsdb': 'bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
config.write(open(config_file, 'w'))
if 'repeater' not in config:
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
config.write(open(config_file, 'w'))
if 'radioMon' not in config:
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
config.write(open(config_file, 'w'))
if 'games' not in config:
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
config.write(open(config_file, 'w'))
if 'messagingSettings' not in config:
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
@@ -58,36 +91,73 @@ else:
# variables
try:
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
location_enabled = config['location'].getboolean('enabled', 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)
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
welcome_message = config['general'].get('welcome_message', WELCOME_MSG)
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
motd_enabled = config['general'].getboolean('motdEnabled', True)
MOTD = config['general'].get('motd', MOTD)
whoami_enabled = config['general'].getboolean('whoami', True)
dad_jokes_enabled = config['general'].getboolean('DadJokes', 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
# sentry
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
# location
location_enabled = config['location'].getboolean('enabled', True)
latitudeValue = config['location'].getfloat('lat', 48.50)
longitudeValue = config['location'].getfloat('lon', -123.0)
zuluTime = config['general'].getboolean('zuluTime', False)
welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG)
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
solar_conditions_enabled = config['solar'].getboolean('enabled', False)
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
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
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
store_forward_enabled = config['general'].getboolean('StoreForward', False)
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
config['general'].get('motd', MOTD)
urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds
forecastDuration = config['general'].getint('DAYS_OF_WEATHER', 4) # default days of weather
numWxAlerts = config['general'].getint('ALERT_COUNT', 2) # default 2 alerts
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
# repeater
repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
radio_dectection_enabled = config['radioMon'].getboolean('enabled', False)
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
sigWatchBrodcastCh = config['radioMon'].get('sigWatchBrodcastCh', '2').split(',') # default Channel 2
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
# games
dopewars_enabled = config['games'].getboolean('dopeWars', True)
lemonade_enabled = config['games'].getboolean('lemonade', True)
blackjack_enabled = config['games'].getboolean('blackjack', True)
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
# messaging settings
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
except KeyError as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")

View File

@@ -1,5 +1,5 @@
# helper functions to get HF band conditions, DRAP X-ray flux, and sunrise/sunset times
# some code from https://github.com/Murturtle/MeshLink
# HF code from https://github.com/Murturtle/MeshLink
# K7MHI Kelly Keeton 2024
import requests # pip install requests
@@ -7,7 +7,7 @@ import xml.dom.minidom
from datetime import datetime
import ephem # pip install pyephem
from datetime import timedelta
from modules.settings import *
from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond")
@@ -19,9 +19,11 @@ def hf_band_conditions():
solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
hf_cond = hf_cond[:-1] # remove the last newline
else:
hf_cond += ERROR_FETCHING_DATA
hf_cond = hf_cond[:-1] # remove the last newline
logger.error("Solar: Error fetching HF band conditions")
hf_cond = ERROR_FETCHING_DATA
return hf_cond
def solar_conditions():
@@ -39,7 +41,8 @@ def solar_conditions():
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
else:
solar_cond += ERROR_FETCHING_DATA
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond
def drap_xray_conditions():
@@ -53,7 +56,8 @@ def drap_xray_conditions():
if x_filter in line:
xray_flux = line.split(": ")[1]
else:
xray_flux += ERROR_FETCHING_DATA
logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux
def get_sun(lat=0, lon=0):

View File

@@ -1,4 +1,4 @@
# helper functions for system related tasks
# helper functions and init for system related tasks
# K7MHI Kelly Keeton 2024
import meshtastic.serial_interface #pip install meshtastic
@@ -6,12 +6,14 @@ import meshtastic.tcp_interface
import meshtastic.ble_interface
import time
import asyncio
import contextlib # for suppressing output on watchdog
from modules.log import *
# Global Variables
trap_list = ("cmd","cmd?") # default trap list
help_message = "CMD?:"
asyncLoop = asyncio.new_event_loop()
games_enabled = False
# Ping Configuration
if ping_enabled:
@@ -20,22 +22,42 @@ if ping_enabled:
trap_list = trap_list + trap_list_ping
help_message = help_message + "ping"
# Sitrep Configuration
if sitrep_enabled:
trap_list_sitrep = ("sitrep", "lheard")
trap_list = trap_list + trap_list_sitrep
help_message = help_message + ", sitrep"
# MOTD Configuration
if motd_enabled:
trap_list_motd = ("motd",)
trap_list = trap_list + trap_list_motd
help_message = help_message + ", motd"
# whoami Configuration
if whoami_enabled:
trap_list_whoami = ("whoami",)
trap_list = trap_list + trap_list_whoami
help_message = help_message + ", whoami"
# Solar Conditions Configuration
if solar_conditions_enabled:
from modules.solarconditions import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
help_message = help_message + ", sun, hfcond, solar, moon, tide"
help_message = help_message + ", sun, hfcond, solar, moon"
# Location Configuration
if location_enabled:
from modules.locationdata import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
help_message = help_message + ", whereami, wx, wxc, wxa"
help_message = help_message + ", whereami, wx, wxc"
# Open-Meteo Configuration for worldwide weather
if use_meteo_wxApi:
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
else:
# NOAA only features
help_message = help_message + ", wxa, tide"
# BBS Configuration
if bbs_enabled:
@@ -49,17 +71,89 @@ if dad_jokes_enabled:
trap_list = trap_list + ("joke",)
help_message = help_message + ", joke"
# Wikipedia Search Configuration
if wikipedia_enabled:
import wikipedia # pip install wikipedia
trap_list = trap_list + ("wiki:",)
help_message = help_message + ", wiki:"
# LLM Configuration
if llm_enabled:
from modules.llm import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_llm # items ask:
help_message = help_message + ", askai"
# DopeWars Configuration
if dopewars_enabled:
from modules.dopewar import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("dopewars",)
games_enabled = True
# Lemonade Stand Configuration
if lemonade_enabled:
from modules.lemonade import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("lemonstand",)
games_enabled = True
# BlackJack Configuration
if blackjack_enabled:
from modules.blackjack import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("blackjack",)
games_enabled = True
# Video Poker Configuration
if videoPoker_enabled:
from modules.videopoker import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("videopoker",)
games_enabled = True
# Games Configuration
if games_enabled is True:
help_message = help_message + ", games"
trap_list = trap_list + ("games",)
gTnW_enabled = True
gamesCmdList = "CMD: "
if dopewars_enabled:
gamesCmdList += "DopeWars, "
if lemonade_enabled:
gamesCmdList += "LemonStand, "
if gTnW_enabled:
import random
trap_list = trap_list + ("globalthermonuclearwar",)
if blackjack_enabled:
gamesCmdList += "BlackJack, "
if videoPoker_enabled:
gamesCmdList += "VideoPoker, "
gamesCmdList = gamesCmdList[:-2] # remove the last comma
# Scheduled Broadcast Configuration
if scheduler_enabled:
import schedule # pip install schedule
# 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"))
# Sentry Configuration
if sentry_enabled:
from math import sqrt
import geopy.distance # pip install geopy
# Store and Forward Configuration
if store_forward_enabled:
trap_list = trap_list + ("messages",)
help_message = help_message + ", messages"
# Radio Monitor Configuration
if radio_dectection_enabled:
if radio_detection_enabled:
from modules.radio import * # from the spudgunman/meshing-around repo
# BLE dual interface prevention
if interface1_type == 'ble' and interface2_type == 'ble':
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
exit()
# Interface1 Configuration
try:
logger.debug(f"System: Initializing Interface1")
if interface1_type == 'serial':
interface1 = meshtastic.serial_interface.SerialInterface(port1)
elif interface1_type == 'tcp':
@@ -70,11 +164,12 @@ try:
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
logger.critical(f"System: script abort. Initalizing Interface1 {e}")
logger.critical(f"System: script abort. Initializing Interface1 {e}")
exit()
# Interface2 Configuration
if interface2_enabled:
logger.debug(f"System: Initializing Interface2")
try:
if interface2_type == 'serial':
interface2 = meshtastic.serial_interface.SerialInterface(port2)
@@ -86,7 +181,7 @@ if interface2_enabled:
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
logger.critical(f"System: script abort. Initalizing Interface2 {e}")
logger.critical(f"System: script abort. Initializing Interface2 {e}")
exit()
#Get the node number of the device, check if the device is connected
@@ -107,6 +202,8 @@ if interface2_enabled:
else:
myNodeNum2 = 777
# functions below
def decimal_to_hex(decimal_number):
return f"!{decimal_number:08x}"
@@ -142,6 +239,40 @@ def get_name_from_number(number, type='long', nodeInt=1):
name = str(decimal_to_hex(number)) # If name not found, use the ID as string
return name
return number
def get_num_from_short_name(short_name, nodeInt=1):
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}")
if nodeInt == 1:
for node in interface1.nodes.values():
#logger.debug(f"System: Checking Node: {node['user']['shortName']} against {short_name} for number {node['num']}")
if short_name == node['user']['shortName']:
return node['num']
elif str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
else:
# try other interface
if interface2_enabled:
for node in interface2.nodes.values():
if short_name == node['user']['shortName']:
return node['num']
elif str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
if nodeInt == 2:
for node in interface2.nodes.values():
if short_name == node['user']['shortName']:
return node['num']
elif str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
else:
# try other interface
if interface2_enabled:
for node in interface1.nodes.values():
if short_name == node['user']['shortName']:
return node['num']
elif str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
return 0
def get_node_list(nodeInt=1):
# Get a list of nodes on the device
@@ -149,6 +280,7 @@ def get_node_list(nodeInt=1):
node_list1 = []
node_list2 = []
short_node_list = []
last_heard = 0
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
@@ -187,23 +319,30 @@ def get_node_list(nodeInt=1):
try:
#print (f"Node List: {node_list1[:5]}\n")
node_list1.sort(key=lambda x: x[1], reverse=True)
node_list1.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
#print (f"Node List: {node_list1[:5]}\n")
node_list2.sort(key=lambda x: x[1], reverse=True)
if interface2_enabled:
node_list2.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
except Exception as e:
logger.error(f"System: Error sorting node list: {e}")
print (f"Node List1: {node_list1[:5]}\n")
print (f"Node List2: {node_list2[:5]}\n")
logger.debug(f"Node List1: {node_list1[:5]}\n")
if interface2_enabled:
logger.debug(f"Node List2: {node_list2[:5]}\n")
node_list = ERROR_FETCHING_DATA
# make a nice list for the user
for x in node_list1[:SITREP_NODE_COUNT]:
short_node_list.append(f"{x[0]} SNR:{x[2]}")
for x in node_list2[:SITREP_NODE_COUNT]:
short_node_list.append(f"{x[0]} SNR:{x[2]}")
try:
# make a nice list for the user
for x in node_list1[:SITREP_NODE_COUNT]:
short_node_list.append(f"{x[0]} SNR:{x[2]}")
for x in node_list2[:SITREP_NODE_COUNT]:
short_node_list.append(f"{x[0]} SNR:{x[2]}")
for x in short_node_list:
if x != "" or x != '\n':
node_list += x + "\n"
for x in short_node_list:
if x != "" or x != '\n':
node_list += x + "\n"
except Exception as e:
logger.error(f"System: Error creating node list: {e}")
node_list = ERROR_FETCHING_DATA
return node_list
@@ -212,6 +351,7 @@ def get_node_location(number, nodeInt=1, channel=0):
latitude = latitudeValue
longitude = longitudeValue
position = [latitudeValue,longitudeValue]
lastheard = 0
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
@@ -227,17 +367,15 @@ def get_node_location(number, nodeInt=1, channel=0):
return position
else:
logger.warning(f"System: No location data for {number} using default location")
# request location data
try:
logger.debug(f"System: Requesting location data for {number}")
if nodeInt == 1:
interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
if nodeInt == 2:
interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
except Exception as e:
logger.error(f"System: Error requesting location data for {number}. Error: {e}")
# try:
# logger.debug(f"System: Requesting location data for {number}")
# if nodeInt == 1:
# interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
# if nodeInt == 2:
# interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
return position
else:
logger.warning(f"System: No nodes found")
@@ -261,9 +399,86 @@ def get_node_location(number, nodeInt=1, channel=0):
else:
logger.warning(f"System: No nodes found")
return position
return position
def get_closest_nodes(nodeInt=1,returnCount=3):
node_list = []
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
pass
# else:
# # request location data
# try:
# logger.debug(f"System: Requesting location data for {node['id']}")
# interface1.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=publicChannel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
if nodeInt == 2:
if interface2.nodes:
for node in interface2.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
pass
# sort by distance closest
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
def send_message(message, ch, nodeid=0, nodeInt=1):
if message == "":
if message == "" or message == None or len(message) == 0:
return
# if message over MESSAGE_CHUNK_SIZE characters, split it into multiple messages
if len(message) > MESSAGE_CHUNK_SIZE:
@@ -278,7 +493,7 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
for word in split_message:
if len(line + word) < MESSAGE_CHUNK_SIZE:
if word == 'NEWLINE':
if 'NEWLINE' in word or '\n' in word or '\r' in word:
# chunk by newline if it exists
message_list.append(line)
line = ''
@@ -289,34 +504,36 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
line = word + ' '
message_list.append(line) # needed add contents of the last 'line' into the list
message_list = [m.replace('NEWLINE', '') for m in message_list]
for m in message_list:
if nodeid == 0:
#Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + f"{m}")
# Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + m.replace('\n', ' '))
if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch)
if nodeInt == 2:
interface2.sendText(text=m, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + f"{m}" + CustomFormatter.purple +\
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch, destinationId=nodeid)
if nodeInt == 2:
interface2.sendText(text=m, channelIndex=ch, destinationId=nodeid)
time.sleep(splitDelay) # wait an amout of time between sending each split message
else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0:
# Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + f"{message}")
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + message.replace('\n', ' '))
if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch)
if nodeInt == 2:
interface2.sendText(text=message, channelIndex=ch)
else:
# Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + f"{message}" + CustomFormatter.purple +\
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch, destinationId=nodeid)
@@ -331,14 +548,30 @@ def tell_joke():
else:
return ''
def messageTrap(msg):
# Check if the message contains a trap word
message_list=msg.split(" ")
for m in message_list:
for t in trap_list:
if t.lower() == m.lower():
return True
return False
def get_wikipedia_summary(search_term):
wikipedia_search = wikipedia.search(search_term, results=3)
wikipedia_suggest = wikipedia.suggest(search_term)
#wikipedia_aroundme = wikipedia.geosearch(location[0], location[1], results=3)
#logger.debug(f"System: Wikipedia Nearby:{wikipedia_aroundme}")
if len(wikipedia_search) == 0:
logger.warning(f"System: No Wikipedia Results for:{search_term}")
return ERROR_FETCHING_DATA
try:
logger.debug(f"System: Searching Wikipedia for:{search_term}, First Result:{wikipedia_search[0]}, Suggest Word:{wikipedia_suggest}")
summary = wikipedia.summary(search_term, sentences=wiki_return_limit, auto_suggest=False, redirect=True)
except wikipedia.DisambiguationError as e:
logger.warning(f"System: Disambiguation Error for:{search_term} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except wikipedia.PageError as e:
logger.warning(f"System: Wikipedia Page Error for:{search_term} {e} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except Exception as e:
logger.error(f"System: Error with Wikipedia for:{search_term} {e}")
return ERROR_FETCHING_DATA
return summary
def messageTrap(msg):
# Check if the message contains a trap word
@@ -351,7 +584,7 @@ def messageTrap(msg):
def exit_handler():
# Close the interface and save the BBS messages
logger.debug(f"\nSystem: Closing Autoresponder\n")
logger.debug(f"System: Closing Autoresponder")
try:
interface1.close()
logger.debug(f"System: Interface1 Closed")
@@ -369,8 +602,14 @@ def exit_handler():
asyncLoop.close()
exit (0)
async def BroadcastScheduler():
# handle schedule checks for the broadcast of messages
while True:
schedule.run_pending()
await asyncio.sleep(1)
async def handleSignalWatcher():
global lastHamLibAlert, antiSpam, sigWatchBrodcastCh
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
# monitor rigctld for signal strength and frequency
while True:
msg = await signalWatcher()
@@ -381,21 +620,21 @@ async def handleSignalWatcher():
if time.time() - lastHamLibAlert > 60:
lastHamLibAlert = time.time()
# if sigWatchBrodcastCh list contains multiple channels, broadcast to all
if type(sigWatchBrodcastCh) is list:
for ch in sigWatchBrodcastCh:
if type(sigWatchBroadcastCh) is list:
for ch in sigWatchBroadcastCh:
if antiSpam and ch != publicChannel:
send_message(msg, int(ch), 0, 1)
if interface2_enabled:
send_message(msg, int(ch), 0, 2)
else:
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
else:
if antiSpam and sigWatchBrodcastCh != publicChannel:
send_message(msg, int(sigWatchBrodcastCh), 0, 1)
if antiSpam and sigWatchBroadcastCh != publicChannel:
send_message(msg, int(sigWatchBroadcastCh), 0, 1)
if interface2_enabled:
send_message(msg, int(sigWatchBrodcastCh), 0, 2)
send_message(msg, int(sigWatchBroadcastCh), 0, 2)
else:
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
await asyncio.sleep(1)
pass
@@ -463,37 +702,54 @@ async def retry_interface(nodeID=1):
except Exception as e:
logger.error(f"System: opening interface2: {e}")
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
from contextlib import contextmanager
import os
import sys
@contextmanager
def suppress_stdout():
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
async def watchdog():
global retry_int1, retry_int2
if sentry_enabled:
sentry_loop = 0
lastSpotted = ""
enemySpotted = ""
sentry_loop2 = 0
lastSpotted2 = ""
enemySpotted2 = ""
# watchdog for connection to the interface
while True:
await asyncio.sleep(20)
#print(f"MeshBot System: watchdog running\r", end="")
if interface1 is not None and not retry_int1:
try:
with suppress_stdout():
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
with contextlib.redirect_stdout(None):
interface1.localNode.getMetadata()
#if "device_state_version:" not in meta:
print(f"System: if you see this upgrade python to >3.4")
except Exception as e:
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
retry_int1 = True
# Locate Closest Nodes and report them to a secure channel
if sentry_enabled:
try:
closest_nodes1 = get_closest_nodes(1)
if closest_nodes1 != ERROR_FETCHING_DATA:
if closest_nodes1[0]['id'] is not None:
enemySpotted = get_name_from_number(closest_nodes1[0]['id'], 'long', 1)
enemySpotted += ", " + get_name_from_number(closest_nodes1[0]['id'], 'short', 1)
enemySpotted += ", " + str(closest_nodes1[0]['id'])
enemySpotted += ", " + decimal_to_hex(closest_nodes1[0]['id'])
enemySpotted += f" at {closest_nodes1[0]['distance']}m"
except Exception as e:
pass
if sentry_loop >= sentry_holdoff and lastSpotted != enemySpotted:
logger.warning(f"System: {enemySpotted} is close to your location on Interface1")
send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 1)
if interface2_enabled:
await asyncio.sleep(1.5)
send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 2)
sentry_loop = 0
lastSpotted = enemySpotted
else:
sentry_loop += 1
if retry_int1:
try:
await retry_interface(1)
@@ -503,15 +759,40 @@ async def watchdog():
if interface2_enabled:
if interface2 is not None and not retry_int2:
try:
with suppress_stdout():
with contextlib.redirect_stdout(None):
interface2.localNode.getMetadata()
print(f"System: if you see this upgrade python to >3.4")
except Exception as e:
logger.error(f"System: communicating with interface2, trying to reconnect: {e}")
retry_int2 = True
# Locate Closest Nodes and report them to a secure channel
if sentry_enabled:
try:
closest_nodes2 = get_closest_nodes(2)
if closest_nodes2 != ERROR_FETCHING_DATA:
if closest_nodes2[0]['id'] is not None:
enemySpotted2 = get_name_from_number(closest_nodes2[0]['id'], 'long', 2)
enemySpotted2 += ", " + get_name_from_number(closest_nodes2[0]['id'], 'short', 2)
enemySpotted2 += ", " + str(closest_nodes2[0]['id'])
enemySpotted2 += ", " + decimal_to_hex(closest_nodes2[0]['id'])
enemySpotted2 += f" at {closest_nodes2[0]['distance']}m"
except Exception as e:
pass
if sentry_loop2 >= sentry_holdoff and lastSpotted2 != enemySpotted2:
logger.warning(f"System: {enemySpotted2} is close to your location on Interface2")
# send to secure channel on both interfaces
send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 1)
await asyncio.sleep(1.5)
send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 2)
sentry_loop2 = 0
lastSpotted2 = enemySpotted2
else:
sentry_loop2 += 1
if retry_int2:
try:
await retry_interface(2)
except Exception as e:
logger.error(f"System: retrying interface2: {e}")

426
modules/videopoker.py Normal file
View File

@@ -0,0 +1,426 @@
# Port of https://github.com/devtronvarma/Video-Poker-Terminal-Game
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
from modules.log import *
vpStartingCash = 20
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
# Define the Card class
class CardVP:
card_values = { # value of the ace is high until it needs to be low
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
10: 10,
'Jack': 11,
'Queen': 12,
'King': 13,
'Ace': 14
}
def __init__(self, suit, rank):
"""
:param suit: The face of the card, e.g. Spade or Diamond
:param rank: The value of the card, e.g 3 or King
"""
self.suit = suit.capitalize()
self.rank = rank
self.points = self.card_values[rank]
# Function to output ascii version of the cards in a hand in the terminal
def drawCardsVp(*cards, return_string=True):
"""
Instead of a boring text version of the card we render an ASCII image of the card.
:param cards: One or more card objects
:param return_string: By default we return the string version of the card, but the dealer hide the 1st card and we
keep it as a list so that the dealer can add a hidden card in front of the list
"""
# we will use this to prints the appropriate icons for each card
suits_name = ['Spades', 'Diamonds', 'Hearts', 'Clubs']
suits_symbols = ['♠️', '♦️', '♥️', '♣️']
# create an empty list of list, each sublist is a line 2 lines for the card
lines = [[] for i in range(1)]
for index, card in enumerate(cards):
# "King" should be "K" and "10" should still be "10"
if card.rank == 10: # ten is the only one who's rank is 2 char long
rank = str(card.rank)
space = '' # if we write "10" on the card that line will be 1 char to long
else:
rank = str(card.rank)[0] # some have a rank of 'King' this changes that to a simple 'K' ("King" doesn't fit)
space = ' ' # no "10", we use a blank space to will the void
# get the cards suit in two steps
suit = suits_name.index(card.suit)
suit = suits_symbols[suit]
# add the individual card on a line by line basis
lines[0].append('{}{} '.format(rank, suit))
result = []
#result.append('1 2 3 4 5') # add the index for the cards to top row
for index, line in enumerate(lines):
result.append(''.join(lines[index]))
# hidden cards do not use string
if return_string:
return '\n'.join(result)
else:
return result
# Define Deck class
class DeckVP:
def __init__(self):
self.cards = []
self.build()
# method for building the deck
def build(self):
for s in ['Spades', 'Diamonds', 'Hearts', 'Clubs']:
for v in range(2, 11):
self.cards.append(CardVP(s,v))
for c in ["Jack", "Queen", "King", "Ace"]:
self.cards.append(CardVP(s,c))
# method to show cards in deck
def display(self):
for c in self.cards:
print(drawCardsVp(c))
# method to shuffle cards in deck
def shuffle(self):
for i in range(len(self.cards) - 1, 0, -1):
r = random.randint(0, i)
self.cards[i], self.cards[r] = self.cards[r], self.cards[i]
# method to draw card from the deck
def draw_card(self):
return self.cards.pop()
# Define Player Class
class PlayerVP:
def __init__(self):
self.hand = []
self.bankroll = 20
# Method for initial five-card draw
def draw_cards(self, deck):
for i in range(5):
self.hand.append(deck.draw_card())
return self
# Method for displaying player's hand
def show_hand(self):
msg = (drawCardsVp(
self.hand[0],
self.hand[1],
self.hand[2],
self.hand[3],
self.hand[4]))
return msg
# Method for placing a bet
def bet(self, ammount=0):
bet = int(ammount)
self.bet_size = bet
self.bankroll -= self.bet_size
# Method for selecting cards to redraw
def redraw(self, deck, message):
# if message has single digit, then it is the card to redraw, else it is the list of cards to redraw with a comma
if len(message) == 1:
try:
# if single digit is the letter a redraw all cards
if message.lower() == "a":
for i in range(5):
self.hand[i] = deck.draw_card()
else:
# error trap for bad input
redraw_index = int(message) - 1
self.hand[redraw_index] = deck.draw_card()
return self.show_hand()
except Exception as e:
pass
else:
try:
# error trap for bad input
if "," in message:
redraw_list = [int(x) - 1 for x in message.split(',')]
if "." in message:
redraw_list = [int(x) - 1 for x in message.split('.')]
if " " in message:
redraw_list = [int(x) - 1 for x in message.split(' ')]
for i in redraw_list:
self.hand[i] = deck.draw_card()
return self.show_hand()
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
def score_hand(self, resetHand = True):
points = sorted([self.hand[i].points for i in range(5)])
suits = [self.hand[i].suit for i in range(5)]
points_repeat = [points.count(i) for i in points]
suits_repeat = [suits.count(i) for i in suits]
diff = max(points) - min(points)
hand_name = ""
msg = ""
payoff = {
"👑Royal Flush🚽": 10,
"🧻Straight Flush🚽": 9,
"Flush🚽": 8,
"Full House🏠": 7,
"Four of a Kind👯👯": 6,
"Three of a Kind☘": 5,
"Two Pair👯👯": 4,
"Straight📏": 3,
"Pair👯": 2,
"Bad Hand 🙈": -1,
}
if 5 in suits_repeat:
if points == [10, 11, 12, 13, 14]: #find royal flush
hand_name = "👑Royal Flush🚽"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif diff == 4 and max(points_repeat) == 1: # find straight flush w/o ace low
hand_name = "🧻Straight Flush🚽"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif diff == 12 and points[4] == 14: # find straight flush w/ace low
check = 0
for i in range(1, 4):
check += points[i] - points[i - 1]
if check == 3:
hand_name = "🧻Straight Flush🚽"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
else:
hand_name = "Flush🚽"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
else:
hand_name = "Flush🚽"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif sorted(points_repeat) == [2,2,3,3,3]: # find full house
hand_name = "Full House🏠"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif 4 in points_repeat: # find four of a kind
hand_name = "Four of a Kind👯👯"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif 3 in points_repeat: # find three of a kind
hand_name = "Three of a Kind☘"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif points_repeat.count(2) == 4: # find two-pair
hand_name = "Two Pair👯👯"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif 2 in points_repeat: # find pair
logger.debug(f"System: VideoPoker: 235 self.bankroll: {self.bankroll} bet_size: {self.bet_size}")
hand_name = "Pair👯"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif diff == 4 and max(points_repeat) == 1: # find straight w/o ace low
hand_name = "Straight📏"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
elif diff == 12 and points[4] == 14: # find straight w/ace low
check = 0
for i in range(1, 4):
check += points[i] - points[i - 1]
if check == 3:
hand_name = "Straight📏"
if resetHand:
self.bankroll += self.bet_size * payoff[hand_name]
else:
hand_name = "Bad Hand 🙈"
else: # for everything Hand
hand_name = "Bad Hand 🙈"
if resetHand:
self.hand = []
msg = f"\nYour hand, {hand_name}. Your bankroll is now {self.bankroll} coins."
else:
if hand_name != "":
msg = f"\nShowing:{hand_name}"
return msg
def getLastCmdVp(nodeID):
last_cmd = ""
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
last_cmd = vpTracker[i]['cmd']
return last_cmd
def setLastCmdVp(nodeID, cmd):
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['cmd'] = cmd
def playVideoPoker(nodeID, message):
msg = ""
# Initialize the player
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID=nodeID) == "":
# create new player if not in tracker
logger.debug(f"System: VideoPoker: New Player {nodeID}")
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
return f"Welcome to 🎰 VideoPoker! you have {vpStartingCash} coins, Whats your bet?"
# Gather the player's bet
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
# Initialize shuffled Deck and Player
player = PlayerVP()
deck = DeckVP()
deck.shuffle()
drawCount = 1
bet = 0
msg = ''
# load the player bankroll from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player.bankroll = vpTracker[i]['cash']
vpTracker[i]['time'] = time.time()
# Detect if message is a bet
try:
bet = int(message)
except ValueError:
msg += "Please enter a valid bet amount. 1 to 5 coins."
# Check if bet is valid
if bet > player.bankroll:
msg += "You can only bet the money you have. No strip poker here..."
elif bet < 1:
msg += "You must bet at least 1 coin."
elif bet > 5:
msg += "You can only bet up to 5 coins."
# if msg contains an error, return it
if msg is not None and msg != '':
return msg
else:
# Take the bet
player.bet(str(message))
# Bet placed, start the game
setLastCmdVp(nodeID, "playing")
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['cash'] = player.bankroll
# Play the game
if getLastCmdVp(nodeID) == "playing":
msg = ''
player.draw_cards(deck)
msg += player.show_hand()
# give hint to player
msg += player.score_hand(resetHand=False)
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll"
setLastCmdVp(nodeID, "redraw")
return msg
if getLastCmdVp(nodeID) == "redraw":
msg = ''
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
drawCount = vpTracker[i]['drawCount']
# if player wants to redraw cards, and not done already
if message.lower().startswith("n"):
setLastCmdVp(nodeID, "endGame")
if message.lower().startswith("h"):
msg = player.show_hand()
return msg
else:
if drawCount <= 1:
msg = player.redraw(deck, message)
if msg.startswith("Send Card"):
# if returned error message, return it
return msg
drawCount += 1
# save player and deck to tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = player
vpTracker[i]['deck'] = deck
vpTracker[i]['drawCount'] = drawCount
if drawCount == 2:
# this is the last draw will carry on to endGame for scoring
msg = player.redraw(deck, message) + f"\n"
if msg.startswith("Send Card"):
# if returned error message, return it
return msg
# redraw done
setLastCmdVp(nodeID, "endGame")
else:
# show redrawn hand
return msg
else:
# redraw already done
setLastCmdVp(nodeID, "endGame")
if getLastCmdVp(nodeID) == "endGame":
# load the player and deck from tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
player = vpTracker[i]['player']
deck = vpTracker[i]['deck']
msg += player.score_hand()
if player.bankroll < 1:
player.bankroll = vpStartingCash
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
elif player.bankroll > vpTracker[i]['highScore']:
vpTracker[i]['highScore'] = player.bankroll
msg += " 🎉HighScore!"
msg += f"\nPlace your Bet, 'L' to leave the game."
setLastCmdVp(nodeID, "gameOver")
# reset player and deck in tracker
for i in range(len(vpTracker)):
if vpTracker[i]['nodeID'] == nodeID:
vpTracker[i]['player'] = None
vpTracker[i]['deck'] = None
vpTracker[i]['drawCount'] = 0
# save bankroll
vpTracker[i]['cash'] = player.bankroll
return msg

177
modules/wx_meteo.py Normal file
View File

@@ -0,0 +1,177 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#import requests_cache
from modules.log import *
def get_wx_meteo(lat=0, lon=0, unit=0):
# set forcast days 1 or 3
forecastDays = 3
# Setup the Open-Meteo API client with cache and retry on error
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
retry_session = retry(retries = 3, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": {lat},
"longitude": {lon},
"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_hours", "precipitation_probability_max", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant"],
"timezone": "auto",
"forecast_days": {forecastDays}
}
# Unit 0 is imperial, 1 is metric
if unit == 0:
params["temperature_unit"] = "fahrenheit"
params["wind_speed_unit"] = "mph"
params["precipitation_unit"] = "inch"
params["distance_unit"] = "mile"
params["pressure_unit"] = "inHg"
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
except Exception as e:
logger.error(f"Error fetching meteo weather data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
response = responses[0]
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
# convert wind value to cardinal directions
for value in daily_wind_direction_10m_dominant:
if value < 22.5:
wind_direction = "N"
elif value < 67.5:
wind_direction = "NE"
elif value < 112.5:
wind_direction = "E"
elif value < 157.5:
wind_direction = "SE"
elif value < 202.5:
wind_direction = "S"
elif value < 247.5:
wind_direction = "SW"
elif value < 292.5:
wind_direction = "W"
elif value < 337.5:
wind_direction = "NW"
else:
wind_direction = "N"
# create a weather report
weather_report = ""
for i in range(forecastDays):
if str(i + 1) == "1":
weather_report += "Today, "
elif str(i + 1) == "2":
weather_report += "Tomorrow, "
else:
weather_report += "Futurecast: "
# report weather from WMO Weather interpretation codes (WW)
code_string = ""
if daily_weather_code[i] == 0:
code_string = "Clear sky"
elif daily_weather_code[i] == 1 or 2 or 3:
code_string = "Partly cloudy"
elif daily_weather_code[i] == 45 or 48:
code_string = "Fog"
elif daily_weather_code[i] == 51:
code_string = "Drizzle: Light"
elif daily_weather_code[i] == 53:
code_string = "Drizzle: Moderate"
elif daily_weather_code[i] == 55:
code_string = "Drizzle: Heavy"
elif daily_weather_code[i] == 56:
code_string = "Freezing Drizzle: Light"
elif daily_weather_code[i] == 57:
code_string = "Freezing Drizzle: Moderate"
elif daily_weather_code[i] == 61:
code_string = "Rain: Slight"
elif daily_weather_code[i] == 63:
code_string = "Rain: Moderate"
elif daily_weather_code[i] == 65:
code_string = "Rain: Heavy"
elif daily_weather_code[i] == 66:
code_string = "Freezing Rain: Light"
elif daily_weather_code[i] == 67:
code_string = "Freezing Rain: Dense"
elif daily_weather_code[i] == 71:
code_string = "Snow: Light"
elif daily_weather_code[i] == 73:
code_string = "Snow: Moderate"
elif daily_weather_code[i] == 75:
code_string = "Snow: Heavy"
elif daily_weather_code[i] == 77:
code_string = "Snow Grains"
elif daily_weather_code[i] == 80:
code_string = "Rain showers: Slight"
elif daily_weather_code[i] == 81:
code_string = "Rain showers: Moderate"
elif daily_weather_code[i] == 82:
code_string = "Rain showers: Heavy"
elif daily_weather_code[i] == 85:
code_string = "Snow showers: Light"
elif daily_weather_code[i] == 86:
code_string = "Snow showers: Moderate"
elif daily_weather_code[i] == 95:
code_string = "Thunderstorm: Slight"
elif daily_weather_code[i] == 96:
code_string = "Thunderstorm: Moderate"
elif daily_weather_code[i] == 99:
code_string = "Thunderstorm: Heavy"
weather_report += "Cond: " + code_string + ". "
# report temperature
if unit == 0:
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "F, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "F. "
else:
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "C, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "C. "
# check for precipitation
if daily_precipitation_hours[i] > 0:
if unit == 0:
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "in, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
else:
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "mm, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
else:
weather_report += "No Precip. "
# check for wind
if daily_wind_speed_10m_max[i] > 0:
if unit == 0:
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "mph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "mph from:" + wind_direction + "."
else:
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "kph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "kph from:" + wind_direction + "."
else:
weather_report += "No Wind\n"
# add a new line for the next day
if i < forecastDays - 1:
weather_report += "\n"
return weather_report

View File

@@ -8,59 +8,111 @@ from pubsub import pub # pip install pubsub
from modules.log import *
from modules.system import *
responseDelay = 0.7 # delay in seconds for response to avoid message collision
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
# Auto response to messages
if "ping" in message.lower():
# Check if the user added @foo to the message
if "@" in message:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
else:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓PONG, " + hop
elif "pong" in message.lower():
bot_response = "🏓Ping!!"
elif "motd" in message.lower():
# check if the user wants to set the motd by using $
if "$" in message:
motd = message.split("$")[1]
global MOTD
MOTD = motd
bot_response = "MOTD Set to: " + MOTD
else:
bot_response = MOTD
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7)
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓Ping!!",
"motd": lambda: handle_motd(message, MOTD),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(hop, snr, rssi),
"test": lambda: handle_testing(hop, snr, rssi),
}
cmds = [] # list to hold the commands found in the message
for key in command_handler:
if key in message_lower.split(' '):
cmds.append({'cmd': key, 'index': message_lower.index(key)})
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}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
def handle_ping(message, hop, snr, rssi):
if "@" in message:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
else:
if hop == "Direct":
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🏓PONG, " + hop
def handle_motd(message):
global MOTD
if "$" in message:
motd = message.split("$")[1]
MOTD = motd.rstrip()
return "MOTD Set to: " + MOTD
else:
return MOTD
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
return bot_response
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "✋ACK-ACK! " + hop
def handle_testing(hop, snr, rssi):
if hop == "Direct":
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🎙Testing 1,2,3 " + hop
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxInterface}")
if port1 in rxInterface:
retry_int1 = True
elif interface2_enabled and port2 in rxInterface:
retry_int2 = True
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
logger.critical(f"System: Lost Connection to Device {rxHost}")
if hostname1 in rxHost and interface1_type == 'tcp':
retry_int1 = True
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
retry_int2 = True
if rxType == 'BLEInterface':
logger.critical(f"System: Lost Connection to Device BLE")
if interface1_type == 'ble':
retry_int1 = True
elif interface2_enabled and interface2_type == 'ble':
retry_int2 = True
def onReceive(packet, interface):
# extract interface defailts from interface object
rxType = type(interface).__name__
@@ -82,11 +134,19 @@ def onReceive(packet, interface):
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# Debug print the packet for debugging
#print(f"Packet Received\n {packet} \n END of packet \n")
message_from_id = 0
# check for a message packet and process it
snr = 0
rssi = 0
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
@@ -174,7 +234,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -185,14 +245,19 @@ def onReceive(packet, interface):
else:
msg_history.pop(0)
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
# check if repeater is enabled and the other interface is enabled
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
# repeat the message on the other device
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
# wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7)
if str(channel_number) in repeater_channels:
if rxNode == 1:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
@@ -200,11 +265,6 @@ 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:
# nothing to do for us
logger.info(f"Ignoring Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
@@ -214,6 +274,7 @@ async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
@@ -221,14 +282,16 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if sentry_enabled:
logger.debug(f"System: Sentry Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
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'))}")
# here we go loopty loo
while True:

View File

@@ -7,3 +7,13 @@ geopy
maidenhead
beautifulsoup4
dadjokes
openmeteo_requests
retry_requests
numpy
geopy
schedule
wikipedia
langchain
langchain-ollama
ollama
googlesearch-python