mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
605 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d425298cd9 | ||
|
|
786815d073 | ||
|
|
54cad92a3f | ||
|
|
54e21f4644 | ||
|
|
3c76f177cd | ||
|
|
aa05c62d94 | ||
|
|
3f16158e27 | ||
|
|
6f2824512d | ||
|
|
723b67f886 | ||
|
|
008d55e63b | ||
|
|
79885454ab | ||
|
|
ba21723bdc | ||
|
|
c36c4918a8 | ||
|
|
853147518d | ||
|
|
2f19d86c95 | ||
|
|
39bdabffcb | ||
|
|
a7bdaedfe1 | ||
|
|
1c6106081f | ||
|
|
8ab6cded2e | ||
|
|
ff63bb959a | ||
|
|
6c79bb1ff0 | ||
|
|
ce73336c0c | ||
|
|
248977c5b5 | ||
|
|
77a6e63210 | ||
|
|
6f6fb35177 | ||
|
|
9db565cb4f | ||
|
|
2a254a7fab | ||
|
|
15e76ab029 | ||
|
|
66b95cdaa0 | ||
|
|
32ea93cb88 | ||
|
|
22a2a64861 | ||
|
|
d841fdf02c | ||
|
|
9421f09ded | ||
|
|
b4af552fb9 | ||
|
|
69dfb17460 | ||
|
|
4703750c27 | ||
|
|
40caf99939 | ||
|
|
df5f648b26 | ||
|
|
55472bbbc0 | ||
|
|
f23c4e2b6a | ||
|
|
0b101d662e | ||
|
|
a7f07afc14 | ||
|
|
2715021898 | ||
|
|
e8fa0036e2 | ||
|
|
f628a5e7ef | ||
|
|
95e6bc120e | ||
|
|
0e35c891c4 | ||
|
|
b7a3e7014c | ||
|
|
0c1c587bc7 | ||
|
|
a0a2c60e63 | ||
|
|
45c912a0d6 | ||
|
|
39945f161d | ||
|
|
ed958302bd | ||
|
|
477f2141d7 | ||
|
|
d321a958f0 | ||
|
|
d14f1df823 | ||
|
|
f7e3b9f6c7 | ||
|
|
cd3ac201f8 | ||
|
|
ceef493b01 | ||
|
|
480a75e30c | ||
|
|
d8cc953fe7 | ||
|
|
0baec88321 | ||
|
|
74bd3f681f | ||
|
|
713b750f4a | ||
|
|
11eee911ca | ||
|
|
b288aaea90 | ||
|
|
7acc018fd2 | ||
|
|
7aba1096f9 | ||
|
|
0be7202144 | ||
|
|
83a5db74e5 | ||
|
|
8dc4371beb | ||
|
|
e5045a0984 | ||
|
|
2c9b37a0cc | ||
|
|
b608482220 | ||
|
|
9290fac899 | ||
|
|
d7901ee575 | ||
|
|
7eb33a5aef | ||
|
|
c5dc103ac0 | ||
|
|
c90172a862 | ||
|
|
8540786c2c | ||
|
|
7aeb8e851d | ||
|
|
2f207dc3d9 | ||
|
|
7e2be73962 | ||
|
|
e2a87eb945 | ||
|
|
4b79c6304f | ||
|
|
22f63e1056 | ||
|
|
bf1613ba66 | ||
|
|
38e04db0cb | ||
|
|
08cc3034a2 | ||
|
|
a0eb176b6e | ||
|
|
a43774b33f | ||
|
|
8369b4d205 | ||
|
|
87e51cf9f9 | ||
|
|
8a8fb79fde | ||
|
|
1dc4cf36b1 | ||
|
|
75cf43e02a | ||
|
|
dd8357453b | ||
|
|
e9b273a62c | ||
|
|
ebebd0fda6 | ||
|
|
3e06902b07 | ||
|
|
a874b42c41 | ||
|
|
f6215d3563 | ||
|
|
4fad58f7fe | ||
|
|
276e4c3d09 | ||
|
|
8aef4d605b | ||
|
|
962c891baa | ||
|
|
eb596ea901 | ||
|
|
e708ec9adc | ||
|
|
c631a083ea | ||
|
|
169ea8c233 | ||
|
|
241e2258e8 | ||
|
|
5d2d6bc5fb | ||
|
|
c7ef22f5c2 | ||
|
|
3ec89decf0 | ||
|
|
2693f47ed5 | ||
|
|
496a13e0e0 | ||
|
|
71ef416dbb | ||
|
|
5bc80c8677 | ||
|
|
8c300f467c | ||
|
|
78b3bf1475 | ||
|
|
ce74f910a7 | ||
|
|
ece531249e | ||
|
|
5052e2510e | ||
|
|
b8f8f80499 | ||
|
|
a13d98e32b | ||
|
|
94996dcec8 | ||
|
|
4b8ce30df8 | ||
|
|
485a37e9b5 | ||
|
|
b41adb12f7 | ||
|
|
a9cd443bdd | ||
|
|
92b9df718f | ||
|
|
a6a4f91d83 | ||
|
|
0fb2c498f4 | ||
|
|
d87e70f7ee | ||
|
|
bee8bae0ae | ||
|
|
d5ef277121 | ||
|
|
dfc8e6c108 | ||
|
|
9100aee91f | ||
|
|
c94cc92d1c | ||
|
|
a41f5e3aca | ||
|
|
cc2d15de0d | ||
|
|
39e08aedae | ||
|
|
fe8730fb1f | ||
|
|
aac3ac8947 | ||
|
|
1acc908bb8 | ||
|
|
47ed98a5e1 | ||
|
|
103034d1b8 | ||
|
|
608714d00a | ||
|
|
575c287860 | ||
|
|
820470bef7 | ||
|
|
ea000ef56c | ||
|
|
793b8f8495 | ||
|
|
373eee3024 | ||
|
|
194273635e | ||
|
|
0972d7d89d | ||
|
|
d1415f9d86 | ||
|
|
b3ff3bb406 | ||
|
|
1efce62a8a | ||
|
|
442f2ff927 | ||
|
|
84cefa1be8 | ||
|
|
45e9c1eccb | ||
|
|
e7e2c9604e | ||
|
|
edaf6875ef | ||
|
|
e7976a0a88 | ||
|
|
e7daae9250 | ||
|
|
91a188f3fd | ||
|
|
49a6d48101 | ||
|
|
a4b5ed3597 | ||
|
|
2efbfeef8c | ||
|
|
1a9093bbdf | ||
|
|
9b3d3c6bdc | ||
|
|
620a6f4795 | ||
|
|
1615fa9e51 | ||
|
|
205906ebb7 | ||
|
|
0d8016651c | ||
|
|
ffdc776413 | ||
|
|
9b92b3d1ae | ||
|
|
3bfea2fd0e | ||
|
|
97636ca575 | ||
|
|
8c4c2095d7 | ||
|
|
9855656e9a | ||
|
|
cffdf92daf | ||
|
|
d07ca4076e | ||
|
|
47740df9a9 | ||
|
|
e830d3fef9 | ||
|
|
3c0a87354e | ||
|
|
b5a2eb9fb9 | ||
|
|
244bd02771 | ||
|
|
95a978495d | ||
|
|
292ad8bb61 | ||
|
|
8b0a2263e7 | ||
|
|
bb9ffe3492 | ||
|
|
ea7ef074e1 | ||
|
|
897d6727b4 | ||
|
|
0f99728be3 | ||
|
|
51903d04f5 | ||
|
|
dde7dbf3a0 | ||
|
|
508998efa8 | ||
|
|
2691247532 | ||
|
|
f3c9d8211f | ||
|
|
ec970d9f08 | ||
|
|
04b86ae7e4 | ||
|
|
b7b31ca5c2 | ||
|
|
7c76e3b3a9 | ||
|
|
863f8d8630 | ||
|
|
b12d16f7a7 | ||
|
|
ead8474783 | ||
|
|
03a1ead721 | ||
|
|
234dbb60fe | ||
|
|
43503df53c | ||
|
|
6c381aecb7 | ||
|
|
8d1e366e10 | ||
|
|
d6a0713f52 | ||
|
|
113bde4bab | ||
|
|
b5354a2431 | ||
|
|
c166dd9ed8 | ||
|
|
80b73da72c | ||
|
|
98490e6444 | ||
|
|
56649471d0 | ||
|
|
0186bdc15a | ||
|
|
eb02014371 | ||
|
|
993292cec8 | ||
|
|
aac99eaf07 | ||
|
|
e5d82df65c | ||
|
|
10bbbecf7b | ||
|
|
8b5f7c4bcb | ||
|
|
10c568fcac | ||
|
|
7443a2fbc4 | ||
|
|
3e5681abc5 | ||
|
|
e5036c0b45 | ||
|
|
a312a81141 | ||
|
|
40dd55d828 | ||
|
|
135bb622e1 | ||
|
|
ed8911be5f | ||
|
|
a126d0610f | ||
|
|
946301ba86 | ||
|
|
604e8ec366 | ||
|
|
2593dd4e86 | ||
|
|
9ee9cdd969 | ||
|
|
ad3918f071 | ||
|
|
3cbc1efa59 | ||
|
|
446f0336b6 | ||
|
|
e3c3becb63 | ||
|
|
8564c85378 | ||
|
|
eedca4c1d0 | ||
|
|
28c46dcbfd | ||
|
|
544c4c3e80 | ||
|
|
8bfd1a4d11 | ||
|
|
3fae6241a7 | ||
|
|
48ff4209c4 | ||
|
|
1d3986c589 | ||
|
|
52b2ec776b | ||
|
|
9d3c6b28ef | ||
|
|
c242eb3750 | ||
|
|
e718fea565 | ||
|
|
14644da9c1 | ||
|
|
0c99dc52d3 | ||
|
|
9cf94d2d1d | ||
|
|
de560f9b8a | ||
|
|
47b8b7a9b8 | ||
|
|
216c468118 | ||
|
|
dd6735167b | ||
|
|
a400eabc0e | ||
|
|
3b95f1b873 | ||
|
|
7b1441814d | ||
|
|
cc39a3a173 | ||
|
|
34df5cfc6a | ||
|
|
37836e7842 | ||
|
|
121231f6fa | ||
|
|
71c8c98ce9 | ||
|
|
2c3cce4d51 | ||
|
|
34ed1247e3 | ||
|
|
e8351a1136 | ||
|
|
4f93e3f5e2 | ||
|
|
33017aa948 | ||
|
|
cbfe68230c | ||
|
|
775c998e4d | ||
|
|
46b8fc7dcd | ||
|
|
d671eb14ce | ||
|
|
cd7d4969ef | ||
|
|
a4e7509354 | ||
|
|
cd74573adc | ||
|
|
86f14ad88a | ||
|
|
847634e524 | ||
|
|
f162019f77 | ||
|
|
b814632f74 | ||
|
|
741ced6289 | ||
|
|
d939a69d16 | ||
|
|
08416cb4e1 | ||
|
|
a30a607f4f | ||
|
|
5caae28b55 | ||
|
|
c85b9d9ee1 | ||
|
|
3d8c8520de | ||
|
|
9e8aec16e5 | ||
|
|
91ea47a6f4 | ||
|
|
c0d108ee77 | ||
|
|
94479f47d9 | ||
|
|
f6fd017871 | ||
|
|
15de706fba | ||
|
|
f7745ac556 | ||
|
|
ad32ed5a80 | ||
|
|
18e1522374 | ||
|
|
bd01da15cc | ||
|
|
1b4ca71e38 | ||
|
|
68fabef2be | ||
|
|
c07f756d67 | ||
|
|
bf52369d2d | ||
|
|
e49e63f39a | ||
|
|
f651f2e577 | ||
|
|
f424da410b | ||
|
|
ada809066c | ||
|
|
e7ec8518e7 | ||
|
|
1ea6a4d987 | ||
|
|
f111aad6b7 | ||
|
|
88282db0e4 | ||
|
|
55e09938bb | ||
|
|
add6c73af7 | ||
|
|
62a932b9ed | ||
|
|
037024395d | ||
|
|
e2105e1fa3 | ||
|
|
be67b33f99 | ||
|
|
7224da9c7a | ||
|
|
fd68007c41 | ||
|
|
e77c7935de | ||
|
|
cf0072fce4 | ||
|
|
6f8a8b4264 | ||
|
|
746db62fdb | ||
|
|
fa4e254ba4 | ||
|
|
0a2e385f41 | ||
|
|
2d6e939306 | ||
|
|
b5d7107760 | ||
|
|
575dd08b74 | ||
|
|
b043541dca | ||
|
|
fa5fd85b5d | ||
|
|
be68cece47 | ||
|
|
50245e618b | ||
|
|
e0d85a65f9 | ||
|
|
a414a994fb | ||
|
|
c0e7f4c0fa | ||
|
|
c03375c7f0 | ||
|
|
453cfbb306 | ||
|
|
deb5ee8374 | ||
|
|
5d2f7dbe8a | ||
|
|
3831998add | ||
|
|
e85982e17c | ||
|
|
46c17c8470 | ||
|
|
5fd326f2e8 | ||
|
|
ad5e086eb5 | ||
|
|
ef9231f51f | ||
|
|
bf34661e42 | ||
|
|
286db4fbea | ||
|
|
4f4dbfbc6f | ||
|
|
1046daaf16 | ||
|
|
b7df9d05a7 | ||
|
|
394c3ef4f6 | ||
|
|
7a071c33cd | ||
|
|
71733de05f | ||
|
|
d5e48a3e36 | ||
|
|
e36755a21d | ||
|
|
9620164884 | ||
|
|
711844cc83 | ||
|
|
6eb82b26a7 | ||
|
|
b12cf6219a | ||
|
|
7e46305277 | ||
|
|
14aa127f31 | ||
|
|
6e7e89c2d0 | ||
|
|
819c37bdec | ||
|
|
c80690d66e | ||
|
|
084f879537 | ||
|
|
7afd6bbbe9 | ||
|
|
46641e8a86 | ||
|
|
010b386ce1 | ||
|
|
a73b320715 | ||
|
|
5f822a6230 | ||
|
|
024fac90cd | ||
|
|
133bc36cca | ||
|
|
51a7ff2820 | ||
|
|
bc96f8df49 | ||
|
|
979f197476 | ||
|
|
1677b69363 | ||
|
|
d627f694df | ||
|
|
4c52cba21f | ||
|
|
597fdd1695 | ||
|
|
9031704b9b | ||
|
|
510a5c5007 | ||
|
|
469e76c50b | ||
|
|
f6c6c58c17 | ||
|
|
e546866f78 | ||
|
|
081566b5d9 | ||
|
|
ec078666ae | ||
|
|
1ce394c7a1 | ||
|
|
2fc3930b43 | ||
|
|
9fa9da5e74 | ||
|
|
d6ad0b5e94 | ||
|
|
15dc50804f | ||
|
|
63c3e35064 | ||
|
|
297930c4d1 | ||
|
|
098c344047 | ||
|
|
4f74677d14 | ||
|
|
0869b19408 | ||
|
|
9b02611700 | ||
|
|
5daa71e6c1 | ||
|
|
aa5f2f66f8 | ||
|
|
92d04f81c3 | ||
|
|
5d53db4211 | ||
|
|
eb3bbdd3c5 | ||
|
|
1ac816ca37 | ||
|
|
33cf18cde5 | ||
|
|
0c0d53dd78 | ||
|
|
1959ee7560 | ||
|
|
ee13401b5a | ||
|
|
78b1cf4af5 | ||
|
|
0599260e31 | ||
|
|
08dd921088 | ||
|
|
e66e938d7d | ||
|
|
b5b7d2a9d2 | ||
|
|
46298d555b | ||
|
|
8fb34b5fde | ||
|
|
28f8986837 | ||
|
|
e968173f61 | ||
|
|
f703a8868b | ||
|
|
0a29e5f156 | ||
|
|
c5c28ee042 | ||
|
|
44ca43399d | ||
|
|
13a47d822d | ||
|
|
5621cd90bb | ||
|
|
9f7055ffd2 | ||
|
|
37a9fc2eb0 | ||
|
|
923325874c | ||
|
|
7ca0c4d744 | ||
|
|
a584a71429 | ||
|
|
70f47635b4 | ||
|
|
8e35d77e07 | ||
|
|
7024f2d472 | ||
|
|
7e2dd4c7ff | ||
|
|
f20d83ca8c | ||
|
|
f31f920137 | ||
|
|
0f428438a3 | ||
|
|
b7882b0322 | ||
|
|
3a417a9281 | ||
|
|
748085c2be | ||
|
|
6a3f56f95f | ||
|
|
f6d6fb7185 | ||
|
|
7865263c1c | ||
|
|
2cf51d5a09 | ||
|
|
f993be950f | ||
|
|
52c4c49bab | ||
|
|
60fdc7b7ea | ||
|
|
a330cff3e5 | ||
|
|
9ffbac7420 | ||
|
|
7909707894 | ||
|
|
8d8014b157 | ||
|
|
a459b7a393 | ||
|
|
7d405dc0c2 | ||
|
|
3decf8749b | ||
|
|
ba6869ec76 | ||
|
|
33cb70ea17 | ||
|
|
69f1b7471f | ||
|
|
76a7d1dba7 | ||
|
|
9f0d3c9d3b | ||
|
|
ff6292160f | ||
|
|
52dcb7972f | ||
|
|
10e2b0ee59 | ||
|
|
473eccbdea | ||
|
|
f6b2e0a506 | ||
|
|
22e16db1f2 | ||
|
|
2c71ca9b8a | ||
|
|
023189bca9 | ||
|
|
8447985b98 | ||
|
|
ad123dc93c | ||
|
|
22983133ee | ||
|
|
60c4a885fd | ||
|
|
95d6d7b7d5 | ||
|
|
37a86b7e2b | ||
|
|
c4ef1251c9 | ||
|
|
9d7e42aa60 | ||
|
|
8536e354ad | ||
|
|
e3faf676cd | ||
|
|
630e016805 | ||
|
|
23b8b8135c | ||
|
|
7f0b4c079a | ||
|
|
47649cdedc | ||
|
|
7915798ca2 | ||
|
|
86cd88910a | ||
|
|
229ccc75f0 | ||
|
|
6f3e3a7957 | ||
|
|
1f1996b909 | ||
|
|
c2069da919 | ||
|
|
458957ddfb | ||
|
|
95c266fbf3 | ||
|
|
4857940165 | ||
|
|
4c780d09e7 | ||
|
|
d616867cd1 | ||
|
|
909c4ad3bc | ||
|
|
44eff643a9 | ||
|
|
a223e57690 | ||
|
|
69bf2d7081 | ||
|
|
c64644a331 | ||
|
|
e8b82ca687 | ||
|
|
47bd8d1d26 | ||
|
|
a6e88a63d5 | ||
|
|
e6be9a7d13 | ||
|
|
8e34925af7 | ||
|
|
1ec6cefc16 | ||
|
|
4a4c5c3e0f | ||
|
|
19e6a38355 | ||
|
|
066f451a4d | ||
|
|
c50776b991 | ||
|
|
8daa9f71e2 | ||
|
|
340cff5e5b | ||
|
|
1747125ea7 | ||
|
|
6ce650dc15 | ||
|
|
d2b303b47c | ||
|
|
74c5bfa64b | ||
|
|
f826c0e4bb | ||
|
|
b8fc3c6c37 | ||
|
|
22b8c8a62e | ||
|
|
f7ad83d2b5 | ||
|
|
fa8b5d6b71 | ||
|
|
036bff1489 | ||
|
|
fe1854f2d8 | ||
|
|
df9a34dc16 | ||
|
|
e762ea4b90 | ||
|
|
3b725837ac | ||
|
|
23efd8e5d8 | ||
|
|
b61463f570 | ||
|
|
8339233459 | ||
|
|
df68111f0c | ||
|
|
b73ad38156 | ||
|
|
2b7d1ed09f | ||
|
|
f1ef5fa787 | ||
|
|
ec14e07513 | ||
|
|
efdd5fab66 | ||
|
|
4fa114a3f2 | ||
|
|
ab64ff14b1 | ||
|
|
65609c5822 | ||
|
|
bdd41c0434 | ||
|
|
80da793c8d | ||
|
|
ba6c296b14 | ||
|
|
9ae95752ad | ||
|
|
9ba430c53c | ||
|
|
9e605a2717 | ||
|
|
aeab22010f | ||
|
|
2d20f4479c | ||
|
|
6546679def | ||
|
|
4dabd20a2e | ||
|
|
d8e5cb7893 | ||
|
|
28514adf00 | ||
|
|
bfa8aa0a86 | ||
|
|
9e205155a5 | ||
|
|
1e921dd5ea | ||
|
|
5c73e49610 | ||
|
|
91f11e4828 | ||
|
|
4a9c969dc0 | ||
|
|
88e960ae33 | ||
|
|
0217f4f2cc | ||
|
|
29fb8b0b40 | ||
|
|
773ee78fb2 | ||
|
|
d43e28d723 | ||
|
|
d063fdd81d | ||
|
|
f73cd5ec31 | ||
|
|
35df43b727 | ||
|
|
e17999a2d6 | ||
|
|
9f658fc060 | ||
|
|
27ece919d7 | ||
|
|
0e97953adf | ||
|
|
66d44c3a6d | ||
|
|
66ca1b4103 | ||
|
|
0b3040f7b7 | ||
|
|
066f7edfd9 | ||
|
|
72f049452b | ||
|
|
c1b493b7c7 | ||
|
|
67af1ba39e | ||
|
|
c48851719a | ||
|
|
cfbda17cfb | ||
|
|
be32fd4a17 | ||
|
|
98b9e0471c | ||
|
|
9efbbb4f20 | ||
|
|
7b8779fc48 | ||
|
|
07e6042e67 | ||
|
|
814303c521 | ||
|
|
2673b638bf | ||
|
|
92b7b7ae2a | ||
|
|
7d63c2dc11 | ||
|
|
514facacd5 | ||
|
|
89dc8791d0 | ||
|
|
700f65ce73 | ||
|
|
4f24701460 | ||
|
|
0514d51aea | ||
|
|
99a05c66ef | ||
|
|
e533e1472e | ||
|
|
ab00cb11bb | ||
|
|
932b98a634 | ||
|
|
b084b0f79e | ||
|
|
115d479020 | ||
|
|
1cb9a60bba | ||
|
|
14c304ca2d | ||
|
|
88d1ecc7ec | ||
|
|
7cabff0bc4 | ||
|
|
5e0ab39301 | ||
|
|
f6ff4e2d7d | ||
|
|
49c0f3b1c5 | ||
|
|
fbd38aa147 | ||
|
|
922956e981 | ||
|
|
ba1447d5f4 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
18
Dockerfile
Normal 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"]
|
||||
146
README.md
146
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
## 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,53 @@ 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`
|
||||
- `bbsinfo` Stats on BBS delivery and messages (sysop)
|
||||
- Other functions
|
||||
- `whereami` returns the address of location of sender if known
|
||||
- `whoami` returns some details of the node asking
|
||||
- `tide` returns the local tides, NOAA data source
|
||||
- `wx` and `wxc` returns local weather forecast, 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`
|
||||
- `history` returns the last commands ran by user(s)
|
||||
- `cmd` returns the list of commands (the help message)
|
||||
- Games - via DM
|
||||
- `lemonstand` plays the classic Lemonade Stand Finance
|
||||
- `dopewars` plays the classic drug trader
|
||||
- `blackjack` BlackJack, Casino 21
|
||||
- `videopoker` Video Poker, basic 5 card hold
|
||||
|
||||
## 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 +105,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 +123,26 @@ enabled = False
|
||||
DadJokes = False
|
||||
StoreForward = False
|
||||
```
|
||||
History command is like a linix terminal, shows the last commands the user ran and the `lheard` reflects last users on the bot.
|
||||
```
|
||||
# history command
|
||||
enableCmdHistory = True
|
||||
# command history ignore list ex: 2813308004,4258675309
|
||||
lheardCmdIgnoreNodes =
|
||||
```
|
||||
Sentry Bot detects anyone coming close to the bot-node
|
||||
```
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryChannel = 9
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryHoldoff = 2
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
```
|
||||
The BBS has admin and block lists; see the [config.template](config.template)
|
||||
|
||||
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
|
||||
@@ -106,8 +159,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 +169,43 @@ 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
|
||||
```
|
||||
|
||||
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 +220,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 +243,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, bitflip, and github Hailo1999, for testing and feature ideas! Lots of individuals on the Meshtastic discord who have tossed out ideas and tested code!
|
||||
|
||||
|
||||
@@ -25,22 +25,59 @@ port = /dev/ttyUSB0
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# defaultChannel is the meshtastic default public channel
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreDefaultChannel = False
|
||||
# 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
|
||||
# history command
|
||||
enableCmdHistory = True
|
||||
# command history ignore list ex: 2813308004,4258675309
|
||||
lheardCmdIgnoreNodes =
|
||||
# 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 +91,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 +113,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 +122,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.0
|
||||
# message chunk size for sending at high success rate
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
|
||||
6
entrypoint.sh
Normal file
6
entrypoint.sh
Normal 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# Load the bbs messages from the database file to screen for admin functions
|
||||
import pickle # pip install pickle
|
||||
|
||||
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('../bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
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")
|
||||
|
||||
print ("\nSystem: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
77
etc/db_admin.py
Normal file
77
etc/db_admin.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Load the bbs messages from the database file to screen for admin functions
|
||||
import pickle # pip install pickle
|
||||
|
||||
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('../bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except Exception as e:
|
||||
bbs_messages = "System: bbsdb.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except Exception as e:
|
||||
bbs_dm = "System: bbsdm.pkl not found"
|
||||
|
||||
# Game HS tables
|
||||
try:
|
||||
with open('../lemonade_hs.pkl', 'rb') as f:
|
||||
lemon_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('lemonade_hs.pkl', 'rb') as f:
|
||||
lemon_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
lemon_score = "System: lemonade_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../dopewar_hs.pkl', 'rb') as f:
|
||||
dopewar_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('dopewar_hs.pkl', 'rb') as f:
|
||||
dopewar_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
dopewar_score = "System: dopewar_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../blackjack_hs.pkl', 'rb') as f:
|
||||
blackjack_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('blackjack_hs.pkl', 'rb') as f:
|
||||
blackjack_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
blackjack_score = "System: blackjack_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../videopoker_hs.pkl', 'rb') as f:
|
||||
videopoker_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('videopoker_hs.pkl', 'rb') as f:
|
||||
videopoker_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
videopoker_score = "System: videopoker_hs.pkl not found"
|
||||
|
||||
print ("\n Meshing-Around Database Admin Tool\n")
|
||||
print ("System: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
print (f"\n\nGame HS tables\n")
|
||||
print (f"lemon:{lemon_score}")
|
||||
print (f"dopewar:{dopewar_score}")
|
||||
print (f"blackjack:{blackjack_score}")
|
||||
print (f"videopoker:{videopoker_score}")
|
||||
print ("\n")
|
||||
@@ -7,8 +7,12 @@ Description=MESH-BOT
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/python /dir/launch.sh mesh
|
||||
ExecStart=python3 mesh_bot.py
|
||||
ExecStop=pkill -f mesh_bot.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
@@ -7,8 +7,12 @@ Description=PONG-BOT
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/python /dir/launch.sh pong
|
||||
ExecStart=python3 pong_bot.py
|
||||
ExecStop=pkill -f pong_bot.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
64
etc/simulator.py
Normal file
64
etc/simulator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/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
|
||||
|
||||
# bot.py Simulated functions
|
||||
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
|
||||
def get_name_from_number(nodeID, length='short', interface=1):
|
||||
# return random name for nodeID
|
||||
names = ["Max","Molly","Jake"]
|
||||
return names[random.randint(0, len(names)-1)]
|
||||
# # 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
|
||||
110
install.sh
110
install.sh
@@ -2,31 +2,77 @@
|
||||
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
|
||||
printf "\nChecking for dependencies\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"
|
||||
|
||||
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
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFpund virtual environment for python\n"
|
||||
else
|
||||
sudo apt-get install python3-venv
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
|
||||
exxt 1
|
||||
fi
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
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(debian/ubuntu)?\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,43 +82,73 @@ 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
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
# set the correct user in the service file?
|
||||
whoami=$(whoami)
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
echo "Do you want to install the emoji font for debian linux? (y/n)"
|
||||
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
exit 0
|
||||
sudo systemctl enable pong_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "mesh" ]; then
|
||||
# install service for mesh bot
|
||||
sudo cp etc/mesh_bot.service /etc/systemd/system/
|
||||
exit 0
|
||||
sudo systemctl enable mesh_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "n" ]; then
|
||||
if [ -f launch.sh ]; then
|
||||
printf "\nTo run the bot, use the command: ./launch.sh\n"
|
||||
./launch.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
|
||||
# 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
|
||||
ollama pull gemma2:2b
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Good time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
|
||||
echo "Goodbye!"
|
||||
exit 0
|
||||
|
||||
@@ -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
|
||||
|
||||
15
logs/README.md
Normal file
15
logs/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Logs will collect here.
|
||||
|
||||
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
|
||||
```
|
||||
1035
mesh_bot.py
1035
mesh_bot.py
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo")
|
||||
|
||||
# global message list, later we will use a pickle on disk
|
||||
bbs_messages = []
|
||||
@@ -16,16 +16,16 @@ def load_bbsdb():
|
||||
try:
|
||||
with open('bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
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)
|
||||
|
||||
@@ -130,6 +130,11 @@ def bbs_post_dm(toNode, message, fromNode):
|
||||
save_bbsdm()
|
||||
return "BBS DM Posted for node " + str(toNode)
|
||||
|
||||
def get_bbs_stats():
|
||||
global bbs_messages, bbs_dm
|
||||
# Return some stats on the bbs pending messages and total posted messages
|
||||
return f"📡BBSdb has {len(bbs_messages)} messages. Direct ✉️ Messages waiting: {(len(bbs_dm) - 1)}"
|
||||
|
||||
def bbs_check_dm(toNode):
|
||||
global bbs_dm
|
||||
# Check for any messages for toNode
|
||||
|
||||
473
modules/blackjack.py
Normal file
473
modules/blackjack.py
Normal file
@@ -0,0 +1,473 @@
|
||||
# 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
|
||||
import pickle
|
||||
|
||||
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
|
||||
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}]
|
||||
|
||||
SUITS = ("♥️", "♦️", "♠️", "♣️")
|
||||
RANKS = (
|
||||
"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 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."
|
||||
else:
|
||||
l_rate = int(rate - (rate - 99)) # Round to 99
|
||||
if card[0][1] == "A":
|
||||
l_rate -= 99
|
||||
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
|
||||
return msg
|
||||
|
||||
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 saveHSJack(nodeID, highScore):
|
||||
# Save the game state to pickle
|
||||
highScore = {'nodeID': nodeID, 'highScore': highScore}
|
||||
try:
|
||||
with open('blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new blackjack_hs.pkl file")
|
||||
with open('blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
|
||||
def loadHSJack():
|
||||
try:
|
||||
with open('blackjack_hs.pkl', 'rb') as file:
|
||||
highScore = pickle.load(file)
|
||||
return highScore
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new blackjack_hs.pkl file")
|
||||
highScore = {'nodeID': 0, 'highScore': 0}
|
||||
with open('blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
def playBlackJack(nodeID, message):
|
||||
# Initalize the Game
|
||||
msg, last_cmd = '', None
|
||||
blackJack = False
|
||||
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']
|
||||
if last_cmd == "playing":
|
||||
p_chips.bet = bet_money
|
||||
p_cards = jackTracker[i]['p_cards']
|
||||
d_cards = jackTracker[i]['d_cards']
|
||||
p_hand = jackTracker[i]['p_hand']
|
||||
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.lower() == "b":
|
||||
if bet_money == 0:
|
||||
bet_money = 5
|
||||
elif message.lower() == "r":
|
||||
#resend the hand
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
return msg
|
||||
else:
|
||||
try:
|
||||
bet_money = int(message)
|
||||
except ValueError:
|
||||
return "Invalid Bet, please enter a valid number."
|
||||
|
||||
if bet_money <= p_chips.total and bet_money >= 1:
|
||||
p_chips.bet = bet_money
|
||||
else:
|
||||
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
except ValueError:
|
||||
return f"Invalid Bet, the maximum bet, {p_chips.total}"
|
||||
|
||||
# Show the cards
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
# check for blackjack 21 and only two cards
|
||||
if p_hand.value == 21 and len(p_hand.cards) == 2:
|
||||
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
|
||||
p_chips.total += round(p_chips.bet * 1.5)
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
blackJack = True
|
||||
# Save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cash'] = int(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,(R)esend,(L)eave table"
|
||||
|
||||
# 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"
|
||||
elif choice == "resend" or choice == "r":
|
||||
msg += show_some(p_hand.cards, d_cards, p_hand)
|
||||
else:
|
||||
return "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
|
||||
# Check if player bust
|
||||
if player_bust(p_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "💥PlayerBUST💥"
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
|
||||
if getLastCmdJack(nodeID) == "playing":
|
||||
msg += stats
|
||||
msg += "[H,S,F,D]"
|
||||
|
||||
# Save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cash'] = int(p_chips.total)
|
||||
jackTracker[i]['bet'] = int(p_chips.bet)
|
||||
jackTracker[i]['gameStats']['p_win'] = int(p_win)
|
||||
jackTracker[i]['gameStats']['d_win'] = int(d_win)
|
||||
jackTracker[i]['gameStats']['draw'] = int(draw)
|
||||
jackTracker[i]['p_cards'] = p_cards
|
||||
jackTracker[i]['d_cards'] = d_cards
|
||||
jackTracker[i]['p_hand'] = p_hand
|
||||
jackTracker[i]['d_hand'] = d_hand
|
||||
break
|
||||
|
||||
# Exit player's turn
|
||||
if getLastCmdJack(nodeID) == "dealerTurn":
|
||||
break
|
||||
|
||||
return msg
|
||||
|
||||
if getLastCmdJack(nodeID) == "dealerTurn":
|
||||
# Dealers Turn
|
||||
if not blackJack:
|
||||
# recall the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
p_chips.total = jackTracker[i]['cash']
|
||||
p_chips.bet = jackTracker[i]['bet']
|
||||
p_win = jackTracker[i]['gameStats']['p_win']
|
||||
d_win = jackTracker[i]['gameStats']['d_win']
|
||||
draw = jackTracker[i]['gameStats']['draw']
|
||||
p_cards = jackTracker[i]['p_cards']
|
||||
d_cards = jackTracker[i]['d_cards']
|
||||
p_hand = jackTracker[i]['p_hand']
|
||||
d_hand = jackTracker[i]['d_hand']
|
||||
next_card = jackTracker[i]['next_card']
|
||||
break
|
||||
|
||||
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 += "💰DealerBUST💥"
|
||||
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 += "👌PUSH"
|
||||
elif player_wins(p_hand, d_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "🎉PLAYER WINS🎰"
|
||||
elif dealer_wins(p_hand, d_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "👎DEALER WINS"
|
||||
else:
|
||||
msg += "👎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 += "🪙Keep the change you filthy animal!"
|
||||
else:
|
||||
msg += "💸NO MORE CHIPS!🏧💳"
|
||||
p_chips.total = jack_starting_cash
|
||||
else:
|
||||
# check high score
|
||||
highScore = loadHSJack()
|
||||
if highScore != 0 and p_chips.total > highScore['highScore']:
|
||||
msg += f"💰HighScore💰{p_chips.total} "
|
||||
saveHSJack(nodeID, p_chips.total)
|
||||
else:
|
||||
msg += f"💰You have {p_chips.total} chips "
|
||||
|
||||
msg += " Bet or Leave?"
|
||||
|
||||
# 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
652
modules/dopewar.py
Normal 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: $" + "{:,}".format(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 = "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 = "You can only sell between 1 and 100"
|
||||
return msg
|
||||
except ValueError:
|
||||
msg = "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: $' + "{:,}".format(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 += ' 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 += "🗺️" + loc[int(loc_choice) - 1] + " 📆" + str(day_play) + '/' + str(total_day) + " 🎒" + str(inventory) + "/100" + " 💵" + "{:,}".format(cash) + f"\n"
|
||||
|
||||
for i, drug in enumerate(my_drugs, 1):
|
||||
qty = amount[i-1]
|
||||
msg += f'#{str(i)}.{drug.name}${"{:,}".format(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 = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
|
||||
menu_choice = cmd.lower()
|
||||
if ',' in menu_choice or '.' in menu_choice:
|
||||
#split the choice into a letter and a number for the buy/sell functions
|
||||
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 = f'a value was bad, example dopeware Buy or Sell\n b,1,10 or s,1,m'
|
||||
return msg
|
||||
|
||||
if menu_choice[0] == 'b':
|
||||
# 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 = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,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💰, (F)ly🛫? (P)riceList?"
|
||||
# set the player's last command
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
|
||||
# Game end
|
||||
if game_day == total_days + 1:
|
||||
msg = endGameDw(nodeID)
|
||||
|
||||
return msg
|
||||
569
modules/lemonade.py
Normal file
569
modules/lemonade.py
Normal file
@@ -0,0 +1,569 @@
|
||||
# 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():
|
||||
high_score = {"userID": 0, "cash": 0, "success": 0}
|
||||
# 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")
|
||||
# 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="LemonStand🍋"
|
||||
# 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
151
modules/llm.py
Normal 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
|
||||
@@ -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,10 @@ def abbreviate_weather(row):
|
||||
"West": "W",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms"
|
||||
"thunderstorms": "t-storms",
|
||||
"thunderstorm": "t-storm",
|
||||
"quarters": "qtrs",
|
||||
"quarter": "qtr"
|
||||
}
|
||||
|
||||
line = row
|
||||
@@ -196,12 +223,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 +263,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 = ""
|
||||
|
||||
@@ -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('logs/system{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler.setFormatter(logging.Formatter(logFormat))
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('logs/messages{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,76 @@ 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)
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', False)
|
||||
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)
|
||||
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,28 +22,59 @@ 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"
|
||||
else:
|
||||
hf_band_conditions = False
|
||||
|
||||
# Command History Configuration
|
||||
if enableCmdHistory:
|
||||
trap_list = trap_list + ("history",)
|
||||
#help_message = help_message + ", history"
|
||||
|
||||
# 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:
|
||||
from modules.bbstools import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_bbs # items bbslist, bbspost, bbsread, bbsdelete, bbshelp
|
||||
help_message = help_message + ", bbslist, bbshelp"
|
||||
else:
|
||||
bbs_help = False
|
||||
bbs_list_messages = False
|
||||
|
||||
|
||||
# Dad Jokes Configuration
|
||||
if dad_jokes_enabled:
|
||||
@@ -49,17 +82,90 @@ 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:", "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 = "Play via DM🕹️ CMD: "
|
||||
if dopewars_enabled:
|
||||
gamesCmdList += "DopeWars, "
|
||||
if lemonade_enabled:
|
||||
gamesCmdList += "LemonStand, "
|
||||
if gTnW_enabled:
|
||||
trap_list = trap_list + ("globalthermonuclearwar",)
|
||||
if blackjack_enabled:
|
||||
gamesCmdList += "BlackJack, "
|
||||
if videoPoker_enabled:
|
||||
gamesCmdList += "VideoPoker, "
|
||||
gamesCmdList = gamesCmdList[:-2] # remove the last comma
|
||||
else:
|
||||
gamesCmdList = ""
|
||||
|
||||
# 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 +176,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 +193,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 +214,8 @@ if interface2_enabled:
|
||||
else:
|
||||
myNodeNum2 = 777
|
||||
|
||||
# functions below
|
||||
|
||||
def decimal_to_hex(decimal_number):
|
||||
return f"!{decimal_number:08x}"
|
||||
|
||||
@@ -142,6 +251,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 +292,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 +331,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 +363,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 +379,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 +411,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 +505,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 +516,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,27 +560,70 @@ 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 getPrettyTime(seconds):
|
||||
# convert unix time to minutes, hours, or days, or years for simple display
|
||||
designator = "s"
|
||||
if seconds > 0:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "m"
|
||||
if seconds > 60:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "h"
|
||||
if seconds > 24:
|
||||
seconds = round(seconds / 24)
|
||||
designator = "d"
|
||||
if seconds > 365:
|
||||
seconds = round(seconds / 365)
|
||||
designator = "y"
|
||||
return str(seconds) + designator
|
||||
|
||||
def messageTrap(msg):
|
||||
# Check if the message contains a trap word
|
||||
# Check if the message contains a trap word, this is the first filter for listning to messages
|
||||
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
|
||||
|
||||
# Split Message on assumed words spaces m for m = msg.split(" ")
|
||||
# t in trap_list, built by the config and system.py not the user
|
||||
message_list=msg.split(" ")
|
||||
for m in message_list:
|
||||
for t in trap_list:
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
return True
|
||||
# if no trap words found, run a search for near misses like ping? or cmd?
|
||||
for m in message_list:
|
||||
for t in range(len(trap_list)):
|
||||
if m.endswith('?') and m[:-1].lower() == trap_list[t]:
|
||||
return True
|
||||
return False
|
||||
|
||||
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 +641,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 +659,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 +741,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 +798,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}")
|
||||
|
||||
|
||||
451
modules/videopoker.py
Normal file
451
modules/videopoker.py
Normal file
@@ -0,0 +1,451 @@
|
||||
# Port of https://github.com/devtronvarma/Video-Poker-Terminal-Game
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
vpStartingCash = 20
|
||||
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
|
||||
|
||||
# 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)
|
||||
else:
|
||||
rank = str(card.rank)[0] # some have a rank of 'King' this changes that to a simple 'K' ("King" doesn't fit)
|
||||
# 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 saveHSVp(nodeID, highScore):
|
||||
# Save the game high_score to pickle
|
||||
highScore = {'nodeID': nodeID, 'highScore': highScore}
|
||||
try:
|
||||
with open('videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new videopoker_hs.pkl file")
|
||||
with open('videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
|
||||
def loadHSVp():
|
||||
# Load the game high_score from pickle
|
||||
try:
|
||||
with open('videopoker_hs.pkl', 'rb') as file:
|
||||
highScore = pickle.load(file)
|
||||
return highScore
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: VideoPoker: Creating new videopoker_hs.pkl file")
|
||||
highScore = {'nodeID': 0, 'highScore': 0}
|
||||
with open('videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
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 (H)and"
|
||||
setLastCmdVp(nodeID, "redraw")
|
||||
return msg
|
||||
|
||||
if getLastCmdVp(nodeID) == "redraw":
|
||||
msg = ''
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
drawCount = vpTracker[i]['drawCount']
|
||||
|
||||
# if player wants to redraw cards, and not done already
|
||||
if message.lower().startswith("n"):
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
if message.lower().startswith("h"):
|
||||
msg = player.show_hand()
|
||||
return msg
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("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!"
|
||||
# save high score
|
||||
saveHSVp(nodeID, vpTracker[i]['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
177
modules/wx_meteo.py
Normal 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
|
||||
|
||||
201
pong_bot.py
201
pong_bot.py
@@ -8,65 +8,127 @@ from pubsub import pub # pip install pubsub
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
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__
|
||||
rxNode = 0
|
||||
# Debug print the interface object
|
||||
#for item in interface.__dict__.items(): print (item)
|
||||
message_from_id = 0
|
||||
snr = 0
|
||||
rssi = 0
|
||||
hop = 0
|
||||
hop_away = 0
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
for item in interface.__dict__.items(): intDebug = f"{item}\n"
|
||||
logger.debug(f"System: Packet Received on {rxType} Interface\n {intDebug} \n END of interface \n")
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
@@ -82,9 +144,11 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
|
||||
# Debug print the packet for debugging
|
||||
#print(f"Packet Received\n {packet} \n END of packet \n")
|
||||
message_from_id = 0
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# check for a message packet and process it
|
||||
try:
|
||||
@@ -174,7 +238,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 +249,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 +269,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,21 +278,24 @@ async def start_rx():
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
if log_messages_to_file:
|
||||
logger.debug(f"System: Logging Messages to disk")
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if sentry_enabled:
|
||||
logger.debug("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")
|
||||
logger.debug("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:
|
||||
|
||||
@@ -7,3 +7,13 @@ geopy
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
langchain
|
||||
langchain-ollama
|
||||
ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user