Compare commits

...

251 Commits
RC7 ... v1.1.2

Author SHA1 Message Date
Kelly
979f197476 Merge pull request #57 from SpudGunMan/llmLocationAware
LLM location aware enhancement
2024-09-04 17:18:03 -07:00
SpudGunMan
1677b69363 comments# 2024-09-04 15:10:38 -07:00
SpudGunMan
d627f694df typo 2024-09-04 15:05:15 -07:00
SpudGunMan
4c52cba21f SpErr 2024-09-04 15:00:44 -07:00
SpudGunMan
597fdd1695 Update llm.py 2024-09-04 14:53:33 -07:00
SpudGunMan
9031704b9b Update llm.py 2024-09-04 14:51:50 -07:00
SpudGunMan
510a5c5007 Update llm.py 2024-09-04 14:42:23 -07:00
SpudGunMan
469e76c50b Update llm.py 2024-09-04 13:14:28 -07:00
SpudGunMan
f6c6c58c17 Update README.md 2024-09-04 11:06:09 -07:00
SpudGunMan
e546866f78 Update llm.py 2024-09-04 09:40:57 -07:00
SpudGunMan
081566b5d9 lower value to speed up query 2024-09-04 09:40:04 -07:00
SpudGunMan
ec078666ae saveSomeAPIcalls 2024-09-04 00:50:57 -07:00
SpudGunMan
1ce394c7a1 Update for LLM 2024-09-04 00:20:06 -07:00
SpudGunMan
2fc3930b43 Update mesh_bot.py 2024-09-03 23:22:02 -07:00
SpudGunMan
9fa9da5e74 Update mesh_bot.py 2024-09-03 23:20:23 -07:00
SpudGunMan
d6ad0b5e94 Update mesh_bot.py 2024-09-03 23:20:13 -07:00
SpudGunMan
15dc50804f Update mesh_bot.py 2024-09-03 23:17:34 -07:00
SpudGunMan
63c3e35064 Update llm.py 2024-09-03 23:12:32 -07:00
SpudGunMan
297930c4d1 Update llm.py 2024-09-03 23:09:45 -07:00
SpudGunMan
098c344047 Update llm.py 2024-09-03 23:08:37 -07:00
SpudGunMan
4f74677d14 Update mesh_bot.py 2024-09-03 23:05:34 -07:00
SpudGunMan
0869b19408 addTimeAware
include the current date in the awareness of location
2024-09-03 23:05:00 -07:00
SpudGunMan
9b02611700 LocationAware 2024-09-03 23:02:04 -07:00
SpudGunMan
5daa71e6c1 llmLocationAware
enhance with local data to the AI
2024-09-03 22:52:27 -07:00
SpudGunMan
aa5f2f66f8 Update llm.py 2024-09-03 21:10:11 -07:00
SpudGunMan
92d04f81c3 contextFromGoogle 2024-09-03 21:08:06 -07:00
SpudGunMan
5d53db4211 enhance 2024-09-03 17:13:04 -07:00
SpudGunMan
eb3bbdd3c5 Update llm.py 2024-09-03 00:48:06 -07:00
SpudGunMan
1ac816ca37 Update README.md 2024-09-03 00:42:38 -07:00
SpudGunMan
33cf18cde5 enhance wiki 2024-09-03 00:29:41 -07:00
SpudGunMan
0c0d53dd78 Update README.md 2024-09-02 23:25:53 -07:00
Kelly
1959ee7560 Merge pull request #53 from mrpatrick1991/docker
Docker
2024-09-02 23:22:51 -07:00
Matthew Patrick
ee13401b5a Update config.template
reset to be identical to main branch
2024-09-02 12:37:30 -06:00
Matthew Patrick
78b1cf4af5 edit docs and make dockerfile use config.ini not config.template 2024-09-02 12:35:44 -06:00
Matthew Patrick
0599260e31 created docker file
docker file and entry point script which copies the values in config.template to the container.
2024-09-02 12:18:20 -06:00
SpudGunMan
08dd921088 gemma2:2b 2024-09-02 11:09:03 -07:00
SpudGunMan
e66e938d7d Update README.md 2024-09-02 11:04:23 -07:00
SpudGunMan
b5b7d2a9d2 Update llm.py 2024-09-02 10:58:13 -07:00
SpudGunMan
46298d555b enhance 2024-09-02 10:47:39 -07:00
SpudGunMan
8fb34b5fde Update config.template 2024-09-02 10:46:31 -07:00
SpudGunMan
28f8986837 Update README.md 2024-09-02 10:46:12 -07:00
SpudGunMan
e968173f61 Update pong_bot.py 2024-09-01 21:53:01 -07:00
SpudGunMan
f703a8868b Update mesh_bot.py 2024-09-01 21:51:21 -07:00
SpudGunMan
0a29e5f156 Update mesh_bot.py 2024-09-01 11:16:01 -07:00
SpudGunMan
c5c28ee042 Update llm.py 2024-09-01 10:57:44 -07:00
SpudGunMan
44ca43399d Update config.template 2024-09-01 09:01:49 -07:00
SpudGunMan
13a47d822d Update config.template 2024-09-01 09:01:00 -07:00
SpudGunMan
5621cd90bb Update config.template 2024-09-01 09:00:44 -07:00
SpudGunMan
9f7055ffd2 model to settings for LLM 2024-09-01 08:59:40 -07:00
SpudGunMan
37a9fc2eb0 Update system.py 2024-09-01 01:12:52 -07:00
SpudGunMan
923325874c Update README.md 2024-09-01 01:10:31 -07:00
SpudGunMan
7ca0c4d744 Update README.md 2024-09-01 01:10:02 -07:00
Kelly
a584a71429 Merge pull request #52 from SpudGunMan/llm
Ollama Module
2024-09-01 01:06:44 -07:00
SpudGunMan
70f47635b4 Update system.py 2024-09-01 01:04:47 -07:00
SpudGunMan
8e35d77e07 Update system.py 2024-09-01 01:00:33 -07:00
SpudGunMan
7024f2d472 Update system.py 2024-09-01 00:58:52 -07:00
SpudGunMan
7e2dd4c7ff Update mesh_bot.py 2024-09-01 00:55:34 -07:00
SpudGunMan
f20d83ca8c Update README.md 2024-09-01 00:48:45 -07:00
SpudGunMan
f31f920137 Update system.py 2024-09-01 00:43:20 -07:00
SpudGunMan
0f428438a3 Update mesh_bot.py 2024-09-01 00:28:28 -07:00
SpudGunMan
b7882b0322 Update mesh_bot.py 2024-09-01 00:17:11 -07:00
SpudGunMan
3a417a9281 Update mesh_bot.py 2024-09-01 00:11:37 -07:00
SpudGunMan
748085c2be Update mesh_bot.py 2024-09-01 00:09:51 -07:00
SpudGunMan
6a3f56f95f enhance 2024-08-31 23:56:55 -07:00
SpudGunMan
f6d6fb7185 enhance 2024-08-31 23:55:33 -07:00
SpudGunMan
7865263c1c Update mesh_bot.py 2024-08-31 23:46:12 -07:00
SpudGunMan
2cf51d5a09 Update system.py 2024-08-31 23:37:23 -07:00
SpudGunMan
f993be950f LLM module 2024-08-31 23:35:03 -07:00
SpudGunMan
52c4c49bab enhance 2024-08-31 23:29:41 -07:00
SpudGunMan
60fdc7b7ea Update system.py 2024-08-31 22:57:37 -07:00
SpudGunMan
a330cff3e5 Update system.py 2024-08-31 22:56:05 -07:00
SpudGunMan
9ffbac7420 Update system.py
random fix
2024-08-31 22:55:12 -07:00
SpudGunMan
7909707894 config enable llm 2024-08-31 22:41:43 -07:00
SpudGunMan
8d8014b157 Update bbstools.py 2024-08-31 22:20:27 -07:00
SpudGunMan
a459b7a393 R&R 2024-08-31 22:11:39 -07:00
SpudGunMan
7d405dc0c2 Update settings.py 2024-08-29 02:42:17 -07:00
SpudGunMan
3decf8749b Update settings.py 2024-08-29 02:41:06 -07:00
SpudGunMan
ba6869ec76 Update system.py 2024-08-28 23:31:32 -07:00
SpudGunMan
33cb70ea17 Update mesh_bot.py 2024-08-28 23:25:21 -07:00
SpudGunMan
69f1b7471f Update mesh_bot.py 2024-08-28 23:22:34 -07:00
SpudGunMan
76a7d1dba7 wikipedia
is this needed? who knows its meshing about!
2024-08-28 23:10:36 -07:00
SpudGunMan
9f0d3c9d3b Update README.md 2024-08-28 12:54:08 -07:00
SpudGunMan
ff6292160f Update mesh_bot.py 2024-08-28 12:43:27 -07:00
SpudGunMan
52dcb7972f Update mesh_bot.py 2024-08-28 12:28:55 -07:00
SpudGunMan
10e2b0ee59 Update system.py 2024-08-27 20:41:35 -07:00
SpudGunMan
473eccbdea fix BLE 2024-08-27 20:31:00 -07:00
SpudGunMan
f6b2e0a506 Update README.md 2024-08-27 19:27:07 -07:00
SpudGunMan
22e16db1f2 typos 2024-08-27 18:10:29 -07:00
SpudGunMan
2c71ca9b8a Update README.md 2024-08-27 18:07:11 -07:00
SpudGunMan
023189bca9 Update README.md 2024-08-27 17:19:18 -07:00
SpudGunMan
8447985b98 Update mesh_bot.py 2024-08-27 17:19:14 -07:00
SpudGunMan
ad123dc93c schedule 2024-08-27 16:58:06 -07:00
SpudGunMan
22983133ee Update mesh_bot.py 2024-08-27 16:44:22 -07:00
SpudGunMan
60c4a885fd Revert "Update mesh_bot.py"
This reverts commit 95d6d7b7d5.
2024-08-27 16:39:16 -07:00
SpudGunMan
95d6d7b7d5 Update mesh_bot.py 2024-08-27 16:24:44 -07:00
SpudGunMan
37a86b7e2b Update system.py 2024-08-27 16:19:52 -07:00
SpudGunMan
c4ef1251c9 enhance code with inital brodcaster
https://github.com/SpudGunMan/meshing-around/issues/51 referenced in this enhancement. this is partially implemented for now in code
2024-08-27 16:06:52 -07:00
SpudGunMan
9d7e42aa60 onDisconnect
add monitor for ondisconnect
2024-08-27 13:08:59 -07:00
SpudGunMan
8536e354ad Update locationdata.py 2024-08-23 22:29:08 -07:00
SpudGunMan
e3faf676cd Update system.py 2024-08-23 22:24:04 -07:00
SpudGunMan
630e016805 Update locationdata.py 2024-08-23 22:24:00 -07:00
SpudGunMan
23b8b8135c Update system.py 2024-08-21 23:13:50 -07:00
SpudGunMan
7f0b4c079a Update README.md 2024-08-21 22:56:42 -07:00
SpudGunMan
47649cdedc Update system.py 2024-08-21 22:48:44 -07:00
SpudGunMan
7915798ca2 Update system.py 2024-08-21 22:46:58 -07:00
SpudGunMan
86cd88910a Update system.py 2024-08-21 22:13:21 -07:00
SpudGunMan
229ccc75f0 Update log.py 2024-08-21 22:00:56 -07:00
SpudGunMan
6f3e3a7957 Update system.py 2024-08-21 21:54:51 -07:00
SpudGunMan
1f1996b909 Update locationdata.py 2024-08-21 21:50:03 -07:00
SpudGunMan
c2069da919 Update locationdata.py 2024-08-21 21:49:29 -07:00
SpudGunMan
458957ddfb ohmyglob 2024-08-21 21:45:12 -07:00
SpudGunMan
95c266fbf3 typo 2024-08-21 21:43:58 -07:00
SpudGunMan
4857940165 Update mesh_bot.py 2024-08-21 21:41:05 -07:00
SpudGunMan
4c780d09e7 fix 2024-08-21 21:40:17 -07:00
SpudGunMan
d616867cd1 Update mesh_bot.py 2024-08-21 21:38:27 -07:00
SpudGunMan
909c4ad3bc Update locationdata.py 2024-08-21 21:31:58 -07:00
SpudGunMan
44eff643a9 Update locationdata.py 2024-08-21 21:27:32 -07:00
SpudGunMan
a223e57690 Update system.py 2024-08-21 20:04:16 -07:00
SpudGunMan
69bf2d7081 enhance sentry with expire out old records
choosing to resolve https://github.com/SpudGunMan/meshing-around/issues/47 with filtering out after 24 hours
2024-08-21 19:37:37 -07:00
SpudGunMan
c64644a331 enhance 2024-08-21 18:32:30 -07:00
SpudGunMan
e8b82ca687 fixes
why did I do it like this..
2024-08-21 18:04:54 -07:00
SpudGunMan
47bd8d1d26 HeartbeatCleanup
Better Code for more secure operations, dropping OS and SYS modules and using a built in. requires Python 3.4 at least for this function.
2024-08-21 17:47:42 -07:00
SpudGunMan
a6e88a63d5 syslog2disk
resolve https://github.com/SpudGunMan/meshing-around/issues/49
2024-08-21 17:30:28 -07:00
SpudGunMan
e6be9a7d13 Update locationdata.py
fix https://github.com/SpudGunMan/meshing-around/issues/44
2024-08-18 09:07:14 -07:00
SpudGunMan
8e34925af7 Update locationdata.py 2024-08-18 01:41:17 -07:00
SpudGunMan
1ec6cefc16 Update mesh_bot.py 2024-08-18 01:26:53 -07:00
SpudGunMan
4a4c5c3e0f Update install.sh 2024-08-17 23:50:08 -07:00
SpudGunMan
19e6a38355 Update install.sh 2024-08-17 23:46:32 -07:00
SpudGunMan
066f451a4d orderLogs 2024-08-16 02:37:38 -07:00
SpudGunMan
c50776b991 aarg 2024-08-15 23:25:31 -07:00
SpudGunMan
8daa9f71e2 fixTypo 2024-08-15 22:52:30 -07:00
SpudGunMan
340cff5e5b Update log.py
fix writing to disk when not wanted
2024-08-14 19:05:28 -07:00
SpudGunMan
1747125ea7 enhance 2024-08-14 18:33:39 -07:00
SpudGunMan
6ce650dc15 Update mesh_bot.py 2024-08-14 17:56:34 -07:00
SpudGunMan
d2b303b47c Update system.py 2024-08-14 17:52:57 -07:00
SpudGunMan
74c5bfa64b Update system.py 2024-08-14 14:47:43 -07:00
SpudGunMan
f826c0e4bb Update locationdata.py 2024-08-14 13:26:28 -07:00
SpudGunMan
b8fc3c6c37 Update system.py 2024-08-14 12:07:44 -07:00
SpudGunMan
22b8c8a62e Update system.py 2024-08-13 16:18:57 -07:00
SpudGunMan
f7ad83d2b5 Update system.py 2024-08-13 16:09:46 -07:00
SpudGunMan
fa8b5d6b71 comments 2024-08-13 16:02:13 -07:00
SpudGunMan
036bff1489 Update system.py 2024-08-13 15:51:41 -07:00
SpudGunMan
fe1854f2d8 Update system.py 2024-08-13 15:22:38 -07:00
SpudGunMan
df9a34dc16 Update system.py 2024-08-13 15:02:40 -07:00
SpudGunMan
e762ea4b90 Update install.sh 2024-08-13 14:23:49 -07:00
SpudGunMan
3b725837ac fixes 2024-08-13 13:57:12 -07:00
SpudGunMan
23efd8e5d8 Update mesh_bot.py 2024-08-13 13:50:53 -07:00
SpudGunMan
b61463f570 Update mesh_bot.py 2024-08-13 13:40:08 -07:00
SpudGunMan
8339233459 Update install.sh 2024-08-13 13:35:30 -07:00
SpudGunMan
df68111f0c Update config.template 2024-08-13 13:34:33 -07:00
SpudGunMan
b73ad38156 Update install.sh
reference https://github.com/SpudGunMan/meshing-around/issues/37
2024-08-13 00:14:50 -07:00
SpudGunMan
2b7d1ed09f Update README.md 2024-08-13 00:00:16 -07:00
SpudGunMan
f1ef5fa787 cleanup 2024-08-12 23:49:50 -07:00
Kelly
ec14e07513 Merge pull request #39 from SpudGunMan/case_test
refactor autoresponse logic
2024-08-12 11:52:50 -07:00
SpudGunMan
efdd5fab66 enhance 2024-08-12 11:40:55 -07:00
SpudGunMan
4fa114a3f2 fix 2024-08-12 03:11:10 -07:00
SpudGunMan
ab64ff14b1 Update mesh_bot.py 2024-08-12 02:59:27 -07:00
SpudGunMan
65609c5822 Update mesh_bot.py 2024-08-12 02:57:34 -07:00
SpudGunMan
bdd41c0434 Update mesh_bot.py 2024-08-12 02:54:32 -07:00
SpudGunMan
80da793c8d Update mesh_bot.py 2024-08-12 02:53:24 -07:00
SpudGunMan
ba6c296b14 Update mesh_bot.py 2024-08-12 02:52:38 -07:00
SpudGunMan
9ae95752ad Update mesh_bot.py 2024-08-12 02:51:34 -07:00
SpudGunMan
9ba430c53c enhance 2024-08-12 02:36:53 -07:00
SpudGunMan
9e605a2717 Update settings.py 2024-08-12 01:23:52 -07:00
SpudGunMan
aeab22010f typo 2024-08-12 00:54:19 -07:00
SpudGunMan
2d20f4479c fixMOTD and settings 2024-08-12 00:43:10 -07:00
SpudGunMan
6546679def rearrange auto if 2024-08-11 23:47:52 -07:00
Kelly
4dabd20a2e Merge pull request #38 from SpudGunMan/sentry
Sentry Mode
2024-08-11 23:04:33 -07:00
SpudGunMan
d8e5cb7893 Update config.template 2024-08-11 23:03:48 -07:00
SpudGunMan
28514adf00 enhance 2024-08-11 23:02:19 -07:00
SpudGunMan
bfa8aa0a86 enhance
fix some issues raised https://github.com/SpudGunMan/meshing-around/issues/37 thankyou!
2024-08-10 12:37:22 -07:00
SpudGunMan
9e205155a5 Update system.py 2024-08-10 09:58:30 -07:00
SpudGunMan
1e921dd5ea geopy tired of maths 2024-08-10 01:48:12 -07:00
SpudGunMan
5c73e49610 Update mesh_bot.py 2024-08-10 00:25:01 -07:00
SpudGunMan
91f11e4828 enhance 2024-08-10 00:17:24 -07:00
SpudGunMan
4a9c969dc0 Update mesh_bot.py 2024-08-10 00:04:42 -07:00
SpudGunMan
88e960ae33 cant handle 🥔 2024-08-10 00:03:08 -07:00
Kelly
0217f4f2cc Merge pull request #35 from SpudGunMan/main
PullinMain
2024-08-09 23:45:56 -07:00
SpudGunMan
29fb8b0b40 Update mesh_bot.py 2024-08-09 23:22:57 -07:00
SpudGunMan
773ee78fb2 Update mesh_bot.py 2024-08-09 23:22:24 -07:00
SpudGunMan
d43e28d723 Update README.md 2024-08-09 23:21:50 -07:00
SpudGunMan
d063fdd81d Update README.md 2024-08-09 23:21:13 -07:00
SpudGunMan
f73cd5ec31 cleanup 2024-08-09 23:19:30 -07:00
SpudGunMan
35df43b727 bbspost by shortname
this is very basic for now
2024-08-09 23:09:21 -07:00
SpudGunMan
e17999a2d6 numpy.. 2024-08-08 23:04:51 -07:00
SpudGunMan
9f658fc060 Update wx_meteo.py 2024-08-08 22:52:04 -07:00
SpudGunMan
27ece919d7 Update system.py 2024-08-08 12:41:29 -07:00
SpudGunMan
0e97953adf Update system.py 2024-08-08 12:32:17 -07:00
SpudGunMan
66d44c3a6d ignorelist 2024-08-08 12:21:55 -07:00
Kelly
66ca1b4103 Merge pull request #34 from SpudGunMan/main
fix depends
2024-08-08 11:54:08 -07:00
SpudGunMan
0b3040f7b7 fix depends 2024-08-08 11:53:27 -07:00
SpudGunMan
066f7edfd9 Revert "fix depends"
This reverts commit 72f049452b.
2024-08-08 11:52:13 -07:00
SpudGunMan
72f049452b fix depends 2024-08-08 11:51:50 -07:00
SpudGunMan
c1b493b7c7 Update system.py 2024-08-08 03:35:43 -07:00
SpudGunMan
67af1ba39e sentry notification 2024-08-08 02:57:52 -07:00
SpudGunMan
c48851719a Update system.py 2024-08-08 02:53:13 -07:00
SpudGunMan
cfbda17cfb Update system.py 2024-08-08 02:06:52 -07:00
SpudGunMan
be32fd4a17 Update system.py 2024-08-08 02:04:21 -07:00
SpudGunMan
98b9e0471c Update README.md 2024-08-08 02:00:39 -07:00
SpudGunMan
9efbbb4f20 Update system.py 2024-08-08 02:00:24 -07:00
SpudGunMan
7b8779fc48 Update system.py 2024-08-08 01:58:39 -07:00
SpudGunMan
07e6042e67 Update system.py 2024-08-08 01:55:55 -07:00
SpudGunMan
814303c521 fix 2024-08-08 01:47:00 -07:00
SpudGunMan
2673b638bf newidea 2024-08-08 01:42:27 -07:00
SpudGunMan
92b7b7ae2a cleanup messageLog 2024-08-08 00:11:12 -07:00
SpudGunMan
7d63c2dc11 tidy output for not printing \n 2024-08-07 22:56:36 -07:00
SpudGunMan
514facacd5 cleanup 2024-08-07 22:46:30 -07:00
SpudGunMan
89dc8791d0 fix debug 2024-08-07 21:55:36 -07:00
SpudGunMan
700f65ce73 Update system.py
resolution for https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 21:53:36 -07:00
SpudGunMan
4f24701460 Update locationdata.py 2024-08-07 21:26:54 -07:00
SpudGunMan
0514d51aea cleanup 2024-08-07 21:24:35 -07:00
Kelly
99a05c66ef Merge pull request #33 from SpudGunMan/worldwx
World Weather
2024-08-07 20:08:23 -07:00
SpudGunMan
e533e1472e fix 2024-08-07 20:06:19 -07:00
SpudGunMan
ab00cb11bb Update system.py 2024-08-07 20:05:27 -07:00
SpudGunMan
932b98a634 debug 2024-08-07 20:04:01 -07:00
SpudGunMan
b084b0f79e Update mesh_bot.py 2024-08-07 19:59:34 -07:00
Kelly
115d479020 Merge branch 'main' into worldwx 2024-08-07 19:53:37 -07:00
SpudGunMan
1cb9a60bba Update system.py 2024-08-07 19:42:48 -07:00
SpudGunMan
14c304ca2d Update system.py 2024-08-07 19:40:41 -07:00
SpudGunMan
88d1ecc7ec Update system.py 2024-08-07 19:40:20 -07:00
SpudGunMan
7cabff0bc4 Update wx_meteo.py 2024-08-07 19:27:47 -07:00
SpudGunMan
5e0ab39301 requirements update 2024-08-07 19:27:29 -07:00
SpudGunMan
f6ff4e2d7d short-strings 2024-08-07 19:20:24 -07:00
SpudGunMan
49c0f3b1c5 Update wx_meteo.py 2024-08-07 19:10:03 -07:00
SpudGunMan
fbd38aa147 Update README.md
addressing issue https://github.com/SpudGunMan/meshing-around/issues/23
2024-08-07 18:52:42 -07:00
SpudGunMan
922956e981 Update wx_meteo.py 2024-08-07 18:12:58 -07:00
SpudGunMan
ba1447d5f4 inital 2024-08-07 18:04:22 -07:00
SpudGunMan
9de72a26d0 bugfix
issue mentioned https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 15:48:23 -07:00
SpudGunMan
cd8a5bafcf cleanup 2024-08-07 12:38:34 -07:00
SpudGunMan
8a7b858edb enhanceRequestLocation 2024-08-07 12:35:01 -07:00
SpudGunMan
ab48622d23 cleanup 2024-08-07 12:05:17 -07:00
SpudGunMan
6eeba2fdbe space 2024-08-07 12:03:16 -07:00
SpudGunMan
b26d0d9f9d fixes 2024-08-07 12:02:32 -07:00
SpudGunMan
cda29f7b16 enhance file log 2024-08-07 11:55:54 -07:00
SpudGunMan
aaca4b5cb4 Update system.py 2024-08-07 11:52:29 -07:00
SpudGunMan
55460ee730 typo 2024-08-06 15:08:25 -07:00
SpudGunMan
94b0102205 enhance 2024-08-06 15:07:35 -07:00
SpudGunMan
dcd1c4235c logMessages 2 Disk 2024-08-06 14:45:39 -07:00
Kelly
4549e6786f Merge pull request #30 from SpudGunMan/cleanup
Cleanup
2024-08-06 14:35:56 -07:00
Kelly
2e7685e1ad Merge pull request #29 from SpudGunMan/main
frescoed
2024-08-06 14:33:08 -07:00
Kelly
4708557bb3 Merge pull request #28 from SpudGunMan/logging
Logging Enhancement and colors
2024-08-06 14:31:10 -07:00
SpudGunMan
2467b2f984 typo 2024-08-06 14:26:17 -07:00
SpudGunMan
fdd94b95b0 Update mesh_bot.py 2024-08-06 14:24:21 -07:00
SpudGunMan
dd3cc524ff enhance 2024-08-06 13:49:51 -07:00
SpudGunMan
0b71ec18a9 uwcolors
i know of corruption at uw
2024-08-06 13:38:45 -07:00
SpudGunMan
2e11d5a4fc Update log.py 2024-08-06 13:27:42 -07:00
SpudGunMan
5cc46fed8f colog
colors and logging
2024-08-06 13:04:05 -07:00
SpudGunMan
191837f1a6 Update mesh_bot.py 2024-08-05 23:30:24 -07:00
SpudGunMan
890843e394 Update system.py 2024-08-05 23:30:11 -07:00
SpudGunMan
85585db723 enhance 2024-08-05 23:20:44 -07:00
SpudGunMan
1719767a47 Update mesh_bot.py 2024-08-05 22:28:38 -07:00
21 changed files with 1641 additions and 455 deletions

18
Dockerfile Normal file
View File

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

136
README.md
View File

@@ -1,22 +1,30 @@
# meshing-around
Random Mesh Scripts for Network Testing and BBS Activities for Use with Meshtastic Nodes
Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
![alt text](etc/pong-bot.jpg "Example Use")
## mesh_bot.sh
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example for further processing.
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example.
Along with network testing, this bot has a lot of other features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM.
Along with network testing, this bot has a lot of other fun features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM. Or a scheduler to send weather or a reminder weekly for the VHF net.
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShportName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages.
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 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
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 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
[Donate$](https://www.paypal.com/donate?token=ZpiU7zDh-AQDyK76nWmWPQLf04iOm-Iyr3f85lpubt37NWGRYtfe11UyC0LmY1wdcC20UubWo4Kec-_G) via PayPal if you like the project!
## 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,14 +32,16 @@ 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 @nodeShportName #message`
- `bbsdelete` delete a message example use: `bbsdelete #4`
- Other functions
- `whereami` returns the address of location of sender if known
- `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`
@@ -41,19 +51,26 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
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 +97,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 +115,19 @@ enabled = False
DadJokes = False
StoreForward = False
```
Sentry Bot detects anyone coming close to the bot-node
```
# detect anyone close to the bot
SentryEnabled = True
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by seconds(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
```
The BBS has admin and block lists; see the [config.template](config.template)
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
@@ -106,8 +144,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 +154,54 @@ 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) however I have found gemma2:2b to be lighter, faster and seems better overall vs llama3,1 (`olamma pull llama3.1`)
- 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)
```
# Enable ollama LLM see more at https://ollama.com
ollama = True
# Ollama model to use (defaults to llama3.1)
ollamaModel = gemma2:2b
```
also see llm.py for changing the defaults of
```
# LLM System Variables
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
```
Logging messages to disk or Syslog to disk uses the python native logging function. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level.
```
[general]
# logging to file of the non Bot messages
LogMessagesToFile = True
# Logging of system messages to file
SyslogToFile = True
```
Example to log to disk only INFO and higher (ignore DEBUG)
```
*log.py
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
```
The Scheduler is enabled in the [settings.py](modules/settings.py) by setting `scheduler_enabled = True` the actions and settings are via code only at this time. see [mesh_bot.py](mesh_bot.py) around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit schedule its most flexible to edit raw code right now. See https://schedule.readthedocs.io/en/stable/ for more.
```
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
```
# requirements
can also be installed with `pip install -r requirements.txt`
Python 3.10 minimally is needed, developed on latest release.
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
```
pip install meshtastic
@@ -132,6 +216,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 +239,9 @@ 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
GitHub user mrpatrick1991 For Docker configs, PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
Discord and Mesh user Cisien, and github Hailo1999, for testing and ideas!

View File

@@ -32,13 +32,37 @@ 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
# enable or disable the Joke module
DadJokes = True
# enable or disable the Solar module
spaceWeather = True
# enable or disable the wikipedia search module
wikipedia = True
# Enable ollama LLM see more at https://ollama.com
ollama = False
# Ollama model to use (defaults to gemma2:2b)
# ollamaModel = llama3.1
# StoreForward Enabled and Limits
StoreForward = True
StoreLimit = 3
# 24 hour clock
zuluTime = True
zuluTime = False
# wait time for URL requests
URL_TIMEOUT = 10
urlTimeout = 10
# logging to file of the non Bot messages
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = False
[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
@@ -52,14 +76,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]
@@ -74,12 +98,12 @@ 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 alsp be = 2,3
sigWatchBroadcastCh = 2
# minimum SNR as reported by radio via hamlib
signalDetectionThreshold = -10
# hold time for high SNR
signalHoldTime = 10
# the following are combined to reset the monitor
signalCooldown = 5
signalCycleLimit = 5
signalCycleLimit = 5

6
entrypoint.sh Normal file
View File

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

View File

@@ -7,13 +7,21 @@ try:
with open('../bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
print ("\nSystem: bbsdb.pkl not found")
try:
with open('bbsdb.pkl', 'rb') as f:
bbs_messages = pickle.load(f)
except:
print ("\nSystem: bbsdb.pkl not found")
try:
with open('../bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
print ("\nSystem: bbsdm.pkl not found")
try:
with open('bbsdm.pkl', 'rb') as f:
bbs_dm = pickle.load(f)
except:
print ("\nSystem: bbsdm.pkl not found")
print ("\nSystem: bbs_messages")
print (bbs_messages)

View File

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

View File

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

View File

@@ -7,26 +7,48 @@ cd "$(dirname "$0")"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
# generate config file
# generate config file, check if it exists
if [ -f config.ini ]; then
printf "\nConfig file already exists, moving to backup config.old\n"
mv config.ini config.old
fi
cp config.template config.ini
printf "\nConfig file generated\n"
# set virtual environment and install dependencies
printf "\nMeshing Around Installer\n"
#check if python3 has venv module
if ! python3 -m venv --help &> /dev/null
then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
else
printf "Python3 venv module found\n"
fi
echo "Do you want to install the bot in a virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
if ! python3 -m venv --help &> /dev/null
then
printf "Python3 venv module not found, please install python3-venv with your OS\n"
exit 1
else
echo "Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
# install dependencies
pip install -U -r requirements.txt
fi
else
printf "\nSkipping virtual environment...\n"
# install dependencies
echo "Are you on Raspberry Pi? should we add --break-system-packages to the pip install command? (y/n)"
printf "Are you on Raspberry Pi?\nshould we add --break-system-packages to the pip install command? (y/n)"
read rpi
if [ $rpi == "y" ]; then
pip install -U -r requirements.txt --break-system-packages
@@ -36,7 +58,7 @@ else
fi
printf "\n\n"
echo "Which bot do you want to install as a service? (pong/mesh/n)"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
read bot
#set the correct path in the service file
@@ -72,7 +94,9 @@ if [ $bot == "n" ]; then
if [ -f launch.sh ]; then
printf "\nTo run the bot, use the command: ./launch.sh\n"
./launch.sh
fi
fi
echo "Goodbye!"
printf "\nGoodbye!"
exit 0

View File

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

View File

@@ -5,159 +5,301 @@
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
from modules.settings import *
from modules.log import *
from modules.system import *
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 "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "pong" in message.lower():
bot_response = "🏓PING!!"
elif "motd" in message.lower():
#check if the user wants to set the motd by using $
if "$" in message:
motd = message.split("$")[1]
global MOTD
MOTD = motd
bot_response = "MOTD Set to: " + MOTD
else:
bot_response = MOTD
elif "messages" in message.lower():
response = ""
for msgH in msg_history:
# check if the message is from the same interface
if msgH[4] == deviceID:
# check if the message is from the same channel
if msgH[2] == channel_number or msgH[2] == publicChannel:
# consider message safe to send
response += f"\n{msgH[0]}: {msgH[1]}"
if len(response) > 0:
bot_response = "Message History:" + response
else:
bot_response = "No messages in history"
elif "bbshelp" in message.lower():
bot_response = bbs_help()
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "sun" in message.lower():
location = get_node_location(message_from_id, deviceID)
bot_response = get_sun(str(location[0]),str(location[1]))
elif "hfcond" in message.lower():
bot_response = hf_band_conditions()
elif "solar" in message.lower():
bot_response = drap_xray_conditions() + "\n" + solar_conditions()
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
elif "whereami" in message.lower():
location = get_node_location(message_from_id, deviceID)
where = where_am_i(str(location[0]),str(location[1]))
bot_response = where
elif "tide" in message.lower():
location = get_node_location(message_from_id, deviceID)
tide = get_tide(str(location[0]),str(location[1]))
bot_response = tide
elif "moon" in message.lower():
location = get_node_location(message_from_id, deviceID)
moon = get_moon(str(location[0]),str(location[1]))
bot_response = moon
elif "wxalert" in message.lower():
location = get_node_location(message_from_id, deviceID)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxa" in message.lower():
location = get_node_location(message_from_id, deviceID)
weatherAlert = getWeatherAlerts(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxc" in message.lower():
location = get_node_location(message_from_id, deviceID)
weather = get_weather(str(location[0]),str(location[1]),1)
bot_response = weather
elif "wx" in message.lower():
location = get_node_location(message_from_id, deviceID)
weather = get_weather(str(location[0]),str(location[1]))
bot_response = weather
elif "joke" in message.lower():
bot_response = tell_joke()
elif "bbslist" in message.lower():
bot_response = bbs_list_messages()
elif "bbspost" in message.lower():
# Check if the user added a subject to the message
if "$" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
print(f"{log_timestamp()} System: BBS Post: {subject} Body: {body}")
bot_response = bbs_post_message(subject,body,message_from_id)
else:
bot_response = "example: bbspost $subject #message"
# Check if the user added a node number to the message
elif "@" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
if "#" in message:
body = message.split("#")[1]
bot_response = bbs_post_dm(toNode, body, message_from_id)
else:
bot_response = "example: bbspost @nodeNumber #message"
else:
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
command_handler = {
"ping": lambda: handle_ping(message, hop, snr, rssi),
"pong": lambda: "🏓PING!!",
"motd": lambda: handle_motd(message),
"bbshelp": bbs_help,
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
"wiki:": lambda: handle_wiki(message),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"joke": tell_joke,
"bbslist": bbs_list_messages,
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
"bbsread": lambda: handle_bbsread(message),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"messages": lambda: handle_messages(deviceID, channel_number, msg_history, publicChannel),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"hfcond": hf_band_conditions,
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"lheard": lambda: handle_lheard(),
"sitrep": lambda: handle_lheard(),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
"ack": lambda: handle_ack(hop, snr, rssi),
"testing": lambda: handle_testing(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']]()
elif "bbsread" in message.lower():
# Check if the user added a message number to the message
if "#" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_read_message(messageID)
else:
bot_response = "Please add a message number ex: bbsread #14"
elif "bbsdelete" in message.lower():
# Check if the user added a message number to the message
if "#" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_delete_message(messageID, message_from_id)
else:
bot_response = "Please add a message number ex: bbsdelete #14"
elif "testing" in message.lower() or "test" in message.lower():
bot_response = "🏓Testing 1,2,3"
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7)
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_wxalert(message_from_id, deviceID, message):
if use_meteo_wxApi:
return "wxalert is not supported"
else:
location = get_node_location(message_from_id, deviceID)
if "wxalert" in message:
# Detailed weather alert
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
return weatherAlert
def handle_wiki(message):
# location = get_node_location(message_from_id, deviceID)
if "wiki:" in message.lower():
search = message.split(":")[1]
search = search.strip()
return get_wikipedia_summary(search)
else:
return "Please add a search term example:wiki: travelling gnome"
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
global llmRunCounter, llmTotalRuntime, llmLocationTable
if location_enabled:
location = get_node_location(message_from_id, deviceID)
# if message_from_id is is the llmLocationTable use the location from the table to save on API calls
if message_from_id in llmLocationTable:
location = llmLocationTable[message_from_id]
else:
location_name = where_am_i(str(location[0]), str(location[1]), short = True)
llmLocationTable.append({message_from_id: location_name})
if NO_DATA_NOGPS in location_name:
location_name = "no location provided "
else:
location_name = "no location provided "
if "ask:" in message.lower():
user_input = message.split(":")[1]
elif "askai" in message.lower():
user_input = message.replace("askai", "")
else:
user_input = message
user_input = user_input.strip()
if len(user_input) < 1:
return "Please ask a question"
# information for the user on how long the query will take on average
if llmRunCounter > 0:
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
if averageRuntime > 25:
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
if channel_number == publicChannel:
send_message(msg, channel_number, message_from_id, deviceID)
else:
send_message(msg, channel_number, 0, deviceID)
else:
msg = "Please wait, response could take 3+ minutes. Fund the SysOp's GPU budget!"
if channel_number == publicChannel:
send_message(msg, channel_number, message_from_id, deviceID)
else:
send_message(msg, channel_number, 0, deviceID)
start = time.time()
#response = asyncio.run(llm_query(user_input, message_from_id))
response = llm_query(user_input, message_from_id, location_name)
# handle the runtime counter
end = time.time()
llmRunCounter += 1
llmTotalRuntime.append(end - start)
return response
def handle_wxc(message_from_id, deviceID, cmd):
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
logger.debug(f"System: Bot Returning Open-Meteo API for weather imperial")
weather = get_wx_meteo(str(location[0]), str(location[1]))
elif use_meteo_wxApi:
logger.debug(f"System: Bot Returning Open-Meteo API for weather metric")
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
logger.debug(f"System: Bot Returning NOAA API for weather metric")
weather = get_weather(str(location[0]), str(location[1]), 1)
else:
logger.debug(f"System: Bot Returning NOAA API for weather imperial")
weather = get_weather(str(location[0]), str(location[1]))
return weather
def handle_bbspost(message, message_from_id, deviceID):
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
logger.info(f"System: BBS Post: {subject} Body: {body}")
return bbs_post_message(subject, body, message_from_id)
elif not "example:" in message:
return "example: bbspost $subject #message"
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
if toNode.isalpha() or not toNode.isnumeric():
toNode = get_num_from_short_name(toNode, deviceID)
if toNode == 0:
return "Node not found " + message.split("@")[1].split("#")[0]
if "#" in message:
body = message.split("#")[1]
return bbs_post_dm(toNode, body, message_from_id)
else:
return "example: bbspost @nodeNumber/ShortName #message"
elif not "example:" in message:
return "example: bbspost $subject #message, or bbspost @node #message"
def handle_bbsread(message):
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
return bbs_read_message(messageID)
elif not "example:" in message:
return "Please add a message number example: bbsread #14"
def handle_bbsdelete(message, message_from_id):
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
return bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
return "Please add a message number example: bbsdelete #14"
def handle_messages(deviceID, channel_number, msg_history, publicChannel):
response = ""
for msgH in msg_history:
if msgH[4] == deviceID:
if msgH[2] == channel_number or msgH[2] == publicChannel:
response += f"\n{msgH[0]}: {msgH[1]}"
if len(response) > 0:
return "Message History:" + response
else:
return "No messages in history"
def handle_sun(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_sun(str(location[0]), str(location[1]))
def handle_lheard():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
return bot_response
def handle_whereami(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return where_am_i(str(location[0]), str(location[1]))
def handle_tide(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_tide(str(location[0]), str(location[1]))
def handle_moon(message_from_id, deviceID, channel_number):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_moon(str(location[0]), str(location[1]))
def handle_ack(hop, snr, rssi):
if hop == "Direct":
return "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
return "🏓ACK-ACK! " + hop
def handle_testing(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
#logger.debug(f"System: Packet Received on {rxType}")
# Debug print the interface object
#for item in interface.__dict__.items(): print (item)
@@ -175,6 +317,12 @@ def onReceive(packet, interface):
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# Debug print the packet for debugging
#print(f"Packet Received\n {packet} \n END of packet \n")
message_from_id = 0
@@ -192,42 +340,42 @@ def onReceive(packet, interface):
if msg:
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
print(f"{log_timestamp()} System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
bbs_delete_dm(msg[0], msg[1])
send_message(message, channel_number, message_from_id, rxNode)
# check for a message packet and process it
snr = 0
rssi = 0
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
try:
snr = packet['rxSnr']
rssi = packet['rxRssi']
except KeyError:
snr = 0
rssi = 0
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
snr = packet.get('rxSnr', 0)
rssi = packet.get('rxRssi', 0)
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = publicChannel
channel_number = packet.get('channel', 0)
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet['hopsAway']
hop_away = packet.get('hopsAway', 0)
else:
# if the packet does not have a hop count try other methods
hop_away = 0
if packet.get('hopLimit'):
hop_limit = packet['hopLimit']
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet['hopStart']
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
@@ -245,26 +393,35 @@ def onReceive(packet, interface):
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# ignore help and welcome messages
print(f"{log_timestamp()} Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
# message is DM to us
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
else:
if llm_enabled:
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
# message is on a channel
if messageTrap(message_string):
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -272,7 +429,7 @@ def onReceive(packet, interface):
# or respond to channel message on the channel itself
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -280,8 +437,8 @@ def onReceive(packet, interface):
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# ignore the message but add it to the message history and repeat it if enabled
# add the message to the message history but limit
# message is not for bot to respond to
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -292,57 +449,102 @@ def onReceive(packet, interface):
else:
msg_history.pop(0)
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
# check if repeater is enabled and the other interface is enabled
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
# repeat the message on the other device
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
# if channel found in the repeater list repeat the message
if str(channel_number) in repeater_channels:
if rxNode == 1:
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
else:
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
except KeyError as e:
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
async def start_rx():
print ("\nMeshtastic Autoresponder Bot CTL+C to exit\n")
if bbs_enabled:
print(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if solar_conditions_enabled:
print(f"System: Celestial Telemetry Enabled")
if location_enabled:
print(f"System: Location Telemetry Enabled")
if dad_jokes_enabled:
print(f"System: Dad Jokes Enabled!")
if store_forward_enabled:
print(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
print(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
print(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
print(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ", myNodeNum1)
logger.debug(f"System: LLM model {llmModel} loaded")
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
msg = (f"{log_timestamp()} 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)}")
print (msg)
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:
msg = (f"{log_timestamp()} 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)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if syslog_to_file:
logger.debug(f"System: Logging System Logs to disk")
if bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if solar_conditions_enabled:
logger.debug(f"System: Celestial Telemetry Enabled")
if location_enabled:
if use_meteo_wxApi:
logger.debug(f"System: Location Telemetry Enabled using Open-Meteo API")
else:
logger.debug(f"System: Location Telemetry Enabled using NOAA API")
if dad_jokes_enabled:
logger.debug(f"System: Dad Jokes Enabled!")
if wikipedia_enabled:
logger.debug(f"System: Wikipedia search Enabled")
if motd_enabled:
logger.debug(f"System: MOTD Enabled using {MOTD}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
#
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
# here we go loopty loo
while True:
@@ -353,7 +555,7 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if radio_dectection_enabled:
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:

View File

@@ -2,7 +2,7 @@
# K7MHI Kelly Keeton 2024
import pickle # pip install pickle
from modules.settings import *
from modules.log import *
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
@@ -18,14 +18,14 @@ def load_bbsdb():
bbs_messages = pickle.load(f)
except:
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
print ("\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
print ("System: Saving bbsdb.pkl\n")
logger.debug("System: Saving bbsdb.pkl")
with open('bbsdb.pkl', 'wb') as f:
pickle.dump(bbs_messages, f)
@@ -64,7 +64,7 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
return "Msg #" + str(messageID) + " deleted."
else:
print (f"!!System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
logger.warning(f"System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
return "You are not authorized to delete this message."
else:
return "Please specify a message number to delete."
@@ -75,12 +75,12 @@ def bbs_post_message(subject, message, fromNode):
# Check the BAN list for naughty nodes and silently drop the message
if str(fromNode) in bbs_ban_list:
print (f"!!System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
print (f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
# save the bbsdb
save_bbsdb()
@@ -100,7 +100,7 @@ def bbs_read_message(messageID = 0):
def save_bbsdm():
global bbs_dm
# save the bbs messages to the database file
print ("System: Saving Updated BBS Direct Messages bbsdm.pkl")
logger.debug("System: Saving Updated BBS Direct Messages bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
@@ -112,7 +112,7 @@ def load_bbsdm():
bbs_dm = pickle.load(f)
except:
bbs_dm = [[1234567890, "Message", 1234567890]]
print ("\nSystem: Creating new bbsdm.pkl")
logger.debug("System: Creating new bbsdm.pkl")
with open('bbsdm.pkl', 'wb') as f:
pickle.dump(bbs_dm, f)
@@ -120,7 +120,7 @@ def bbs_post_dm(toNode, message, fromNode):
global bbs_dm
# Check the BAN list for naughty nodes and silently drop the message
if str(fromNode) in bbs_ban_list:
print (f"!!System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
return "DM Posted for node " + str(toNode)
# append the message to the list

146
modules/llm.py Normal file
View File

@@ -0,0 +1,146 @@
#!/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/
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 error: {e}")
googleResults = ['no other context provided']
if googleResults:
logger.debug(f"System: External LLM Query: {input} From:{nodeID} with context from google")
else:
logger.debug(f"System: External LLM Query: {input} From:{nodeID}")
response = ""
result = ""
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
try:
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \
"history": llmChat_history, "context": googleResults, "location_name": location_name})
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
logger.warning(f"System: LLM failure: {e}")
return "I am having trouble processing your request, please try again later."
response = result.strip().replace('\n', ' ')
# Store history of the conversation, with limit to prevent template growing too large causing speed issues
if len(llmChat_history) > llm_history_limit:
# remove the oldest two messages
llmChat_history.pop(0)
llmChat_history.pop(1)
inputWithUserID = input + f" user={nodeID}"
llmChat_history.append(HumanMessage(content=inputWithUserID))
llmChat_history.append(AIMessage(content=response))
# done with the query, remove the user from the anti flood list
antiFloodLLM.remove(nodeID)
return response
# import subprocess
# def get_ollama_cpu():
# try:
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
# if "GPU" in psOutput.stdout:
# logger.debug(f"System: Ollama process with GPU")
# else:
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False

View File

@@ -1,4 +1,4 @@
# helper functions to use location data
# helper functions to use location data like NOAA weather
# K7MHI Kelly Keeton 2024
import json # pip install json
@@ -7,21 +7,29 @@ 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 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)
@@ -29,18 +37,18 @@ def where_am_i(lat=0, lon=0):
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
return whereIam
return whereIam
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 +56,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 +76,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 +110,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 +121,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 +198,9 @@ def abbreviate_weather(row):
"West": "W",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms"
"thunderstorms": "t-storms",
"quarters": "qtrs",
"quarter": "qtr"
}
line = row
@@ -196,12 +217,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 +257,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 = ""

72
modules/log.py Normal file
View File

@@ -0,0 +1,72 @@
# 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 *
class CustomFormatter(logging.Formatter):
grey = '\x1b[38;21m'
white = '\x1b[38;5;231m'
blue = '\x1b[38;5;39m'
yellow = '\x1b[38;5;226m'
red = '\x1b[38;5;196m'
green = '\x1b[38;5;46m'
purple = '\x1b[38;5;129m'
bold_red = '\x1b[31;1m'
bold_white = '\x1b[37;1m'
reset = '\x1b[0m'
def __init__(self, fmt):
super().__init__()
self.fmt = fmt
self.FORMATS = {
logging.DEBUG: self.blue + self.fmt + self.reset,
logging.INFO: self.white + self.fmt + self.reset,
logging.WARNING: self.yellow + self.fmt + self.reset,
logging.ERROR: self.red + self.fmt + self.reset,
logging.CRITICAL: self.bold_red + self.fmt + self.reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# Create logger
logger = logging.getLogger("MeshBot System Logger")
logger.setLevel(logging.DEBUG)
logger.propagate = False
msgLogger = logging.getLogger("MeshBot Messages Logger")
msgLogger.setLevel(logging.INFO)
msgLogger.propagate = False
# Define format for logs
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
msgLogFormat = '%(asctime)s | %(message)s'
today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
# Set level for stdout handler (logs DEBUG level and above)
stdout_handler.setLevel(logging.DEBUG)
# Set format for stdout handler
stdout_handler.setFormatter(CustomFormatter(logFormat))
# Add handlers to the logger
logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('system{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
file_handler.setFormatter(logging.Formatter(logFormat))
logger.addHandler(file_handler)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)

View File

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

View File

@@ -1,3 +1,5 @@
# Settings for MeshBot and PongBot
# 2024 Kelly Keeton K7MHI
import configparser
# messages
@@ -22,6 +24,11 @@ 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
llmRunCounter = 0
llmTotalRuntime = []
llmLocationTable = []
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -40,6 +47,26 @@ 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'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
@@ -58,38 +85,56 @@ else:
# variables
try:
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
location_enabled = config['location'].getboolean('enabled', False)
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
syslog_to_file = config['general'].getboolean('SyslogToFile', False)
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
store_forward_enabled = config['general'].getboolean('StoreForward', True)
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
welcome_message = config['general'].get('welcome_message', WELCOME_MSG)
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
motd_enabled = config['general'].getboolean('motdEnabled', True)
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_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_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_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)
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_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
radio_dectection_enabled = config['radioMon'].getboolean('enabled', False)
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
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.")
print(f"System: Exiting...")
exit(1)

View File

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

View File

@@ -1,13 +1,13 @@
# 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
import meshtastic.tcp_interface
import meshtastic.ble_interface
from datetime import datetime
import time
import asyncio
from modules.settings import *
import contextlib # for suppressing output on watchdog
from modules.log import *
# Global Variables
trap_list = ("cmd","cmd?") # default trap list
@@ -21,22 +21,36 @@ 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"
# Solar Conditions Configuration
if solar_conditions_enabled:
from modules.solarconditions import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
help_message = help_message + ", sun, hfcond, solar, moon, tide"
help_message = help_message + ", sun, hfcond, solar, moon"
# Location Configuration
if location_enabled:
from modules.locationdata import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
help_message = help_message + ", whereami, wx, wxc, wxa"
help_message = help_message + ", whereami, wx, wxc"
# Open-Meteo Configuration for worldwide weather
if use_meteo_wxApi:
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
else:
# NOAA only features
help_message = help_message + ", wxa, tide"
# BBS Configuration
if bbs_enabled:
@@ -50,17 +64,46 @@ if dad_jokes_enabled:
trap_list = trap_list + ("joke",)
help_message = help_message + ", joke"
# Wikipedia Search Configuration
if wikipedia_enabled:
import wikipedia # pip install wikipedia
trap_list = trap_list + ("wiki:",)
help_message = help_message + ", wiki:"
# LLM Configuration
if llm_enabled:
from modules.llm import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_llm # items ask:
help_message = help_message + ", askai"
# 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':
@@ -68,14 +111,15 @@ try:
elif interface1_type == 'ble':
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
else:
print(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
print(f"System: Critical Error 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)
@@ -84,10 +128,10 @@ if interface2_enabled:
elif interface2_type == 'ble':
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
else:
print(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
exit()
except Exception as e:
print(f"System: Critical Error 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
@@ -95,7 +139,7 @@ try:
myinfo = interface1.getMyNodeInfo()
myNodeNum1 = myinfo['num']
except Exception as e:
print(f"System: Critical Error script abort. {e}")
logger.critical(f"System: script abort. {e}")
exit()
if interface2_enabled:
@@ -103,16 +147,12 @@ if interface2_enabled:
myinfo2 = interface2.getMyNodeInfo()
myNodeNum2 = myinfo2['num']
except Exception as e:
print(f"System: Critical Error script abort. {e}")
logger.critical(f"System: script abort. {e}")
exit()
else:
myNodeNum2 = 777
def log_timestamp():
if zuluTime:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
return datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
# functions below
def decimal_to_hex(decimal_number):
return f"!{decimal_number:08x}"
@@ -149,6 +189,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
@@ -156,6 +230,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():
@@ -171,7 +246,7 @@ def get_node_list(nodeInt=1):
item = (node_name, last_heard, snr)
node_list1.append(item)
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return ERROR_FETCHING_DATA
if nodeInt == 2:
@@ -189,30 +264,44 @@ def get_node_list(nodeInt=1):
item = (node_name, last_heard, snr)
node_list2.append(item)
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return ERROR_FETCHING_DATA
node_list1.sort(key=lambda x: x[1], reverse=True)
#print (f"Node List: {node_list1[:5]}\n")
node_list2.sort(key=lambda x: x[1], reverse=True)
try:
#print (f"Node List: {node_list1[:5]}\n")
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")
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}")
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
def get_node_location(number, nodeInt=1):
def get_node_location(number, nodeInt=1, channel=0):
# Get the location of a node by its number from nodeDB on device
latitude = latitudeValue
longitude = longitudeValue
position = [latitudeValue,longitudeValue]
lastheard = 0
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
@@ -222,15 +311,24 @@ def get_node_location(number, nodeInt=1):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
except Exception as e:
print (f"{log_timestamp()} System: Error getting location data for {number}")
print (f"System: location data for {number} is {latitude},{longitude}")
logger.error(f"System: Error getting location data for {number}")
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
position = [latitude,longitude]
return position
else:
print (f"{log_timestamp()} System: No location data for {number}")
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}")
return position
else:
print (f"{log_timestamp()} System: No nodes found")
logger.warning(f"System: No nodes found")
return position
if nodeInt == 2:
if interface2.nodes:
@@ -241,21 +339,100 @@ def get_node_location(number, nodeInt=1):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
except Exception as e:
print (f"{log_timestamp()} System: Error getting location data for {number}")
print (f"System: location data for {number} is {latitude},{longitude}")
logger.error(f"System: Error getting location data for {number}")
logger.info(f"System: location data for {number} is {latitude},{longitude}")
position = [latitude,longitude]
return position
else:
print (f"{log_timestamp()} System: No location data for {number}")
logger.warning(f"System: No location data for {number}")
return position
else:
print (f"{log_timestamp()} System: No nodes found")
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 == "" 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:
print (f"{log_timestamp()} System: Splitting Message, Message Length: {len(message)}")
logger.debug(f"System: Splitting Message, Message Length: {len(message)}")
# split the message into MESSAGE_CHUNK_SIZE 160 character chunks
message = message.replace('\n', ' NEWLINE ') # replace newlines with NEWLINE to keep them in split chunks
@@ -266,7 +443,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 = ''
@@ -277,18 +454,20 @@ 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
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Multi-Chunk Message: {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
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} Multi-Chunk Message: {m} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
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:
@@ -296,14 +475,15 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0:
# Send to channel
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Message: {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
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} {message} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
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)
if nodeInt == 2:
@@ -317,14 +497,30 @@ def tell_joke():
else:
return ''
def messageTrap(msg):
# Check if the message contains a trap word
message_list=msg.split(" ")
for m in message_list:
for t in trap_list:
if t.lower() == m.lower():
return True
return False
def get_wikipedia_summary(search_term):
wikipedia_search = wikipedia.search(search_term, results=3)
wikipedia_suggest = wikipedia.suggest(search_term)
#wikipedia_aroundme = wikipedia.geosearch(location[0], location[1], results=3)
#logger.debug(f"System: Wikipedia Nearby:{wikipedia_aroundme}")
if len(wikipedia_search) == 0:
logger.warning(f"System: No Wikipedia Results for:{search_term}")
return ERROR_FETCHING_DATA
try:
logger.debug(f"System: Searching Wikipedia for:{search_term}, First Result:{wikipedia_search[0]}, Suggest Word:{wikipedia_suggest}")
summary = wikipedia.summary(search_term, sentences=wiki_return_limit, auto_suggest=False, redirect=True)
except wikipedia.DisambiguationError as e:
logger.warning(f"System: Disambiguation Error for:{search_term} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except wikipedia.PageError as e:
logger.warning(f"System: Wikipedia Page Error for:{search_term} {e} trying {wikipedia_search[0]}")
summary = wikipedia.summary(wikipedia_search[0], sentences=wiki_return_limit, auto_suggest=True, redirect=True)
except Exception as e:
logger.error(f"System: Error with Wikipedia for:{search_term} {e}")
return ERROR_FETCHING_DATA
return summary
def messageTrap(msg):
# Check if the message contains a trap word
@@ -337,52 +533,57 @@ def messageTrap(msg):
def exit_handler():
# Close the interface and save the BBS messages
print(f"\n{log_timestamp()} System: Closing Autoresponder\n")
logger.debug(f"System: Closing Autoresponder")
try:
interface1.close()
print(f"{log_timestamp()} System: Interface1 Closed")
logger.debug(f"System: Interface1 Closed")
if interface2_enabled:
interface2.close()
print(f"{log_timestamp()} System: Interface2 Closed")
logger.debug(f"System: Interface2 Closed")
except Exception as e:
print(f"{log_timestamp()} System: Error closing: {e}")
logger.error(f"System: closing: {e}")
if bbs_enabled:
save_bbsdb()
save_bbsdm()
print(f"{log_timestamp()} System: BBS Messages Saved")
print(f"{log_timestamp()} System: Exiting")
logger.debug(f"System: BBS Messages Saved")
logger.debug(f"System: Exiting")
asyncLoop.stop()
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()
if msg != ERROR_FETCHING_DATA and msg is not None:
print(f"{log_timestamp()} System: Detected Alert from Hamlib {msg}")
logger.debug(f"System: Detected Alert from Hamlib {msg}")
# check we are not spammig the channel limit messages to once per minute
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:
print(f"{log_timestamp()} 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:
print(f"{log_timestamp()} System: antiSpam prevented Alert from Hamlib {msg}")
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
await asyncio.sleep(1)
pass
@@ -398,7 +599,7 @@ async def retry_interface(nodeID=1):
try:
interface1.close()
except Exception as e:
print(f"{log_timestamp()} System: Error closing interface1: {e}")
logger.error(f"System: closing interface1: {e}")
if nodeID==2:
if interface2 is not None:
retry_int2 = True
@@ -406,15 +607,15 @@ async def retry_interface(nodeID=1):
try:
interface2.close()
except Exception as e:
print(f"{log_timestamp()} System: Error closing interface2: {e}")
logger.error(f"System: closing interface2: {e}")
print(f"{log_timestamp()} System: Retrying interface in 15 seconds")
logger.debug(f"System: Retrying interface in 15 seconds")
if max_retry_count1 == 0:
print(f"{log_timestamp()} System: Max retry count reached for interface1")
logger.critical(f"System: Max retry count reached for interface1")
exit_handler()
if max_retry_count2 == 0:
print(f"{log_timestamp()} System: Max retry count reached for interface2")
logger.critical(f"System: Max retry count reached for interface2")
exit_handler()
# wait 15 seconds before retrying
await asyncio.sleep(15)
@@ -423,82 +624,124 @@ async def retry_interface(nodeID=1):
try:
if nodeID==1 and retry_int1:
interface1 = None
print(f"{log_timestamp()} System: Retrying Interface1")
logger.debug(f"System: Retrying Interface1")
if interface1_type == 'serial':
interface1 = meshtastic.serial_interface.SerialInterface(port1)
elif interface1_type == 'tcp':
interface1 = meshtastic.tcp_interface.TCPInterface(hostname1)
elif interface1_type == 'ble':
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
print(f"{log_timestamp()} System: Interface1 Opened!")
logger.debug(f"System: Interface1 Opened!")
retry_int1 = False
except Exception as e:
print(f"{log_timestamp()} System: Error opening interface1 on: {e}")
logger.error(f"System: opening interface1 on: {e}")
try:
if nodeID==2 and retry_int2:
interface2 = None
print(f"{log_timestamp()} System: Retrying Interface2")
logger.debug(f"System: Retrying Interface2")
if interface2_type == 'serial':
interface2 = meshtastic.serial_interface.SerialInterface(port2)
elif interface2_type == 'tcp':
interface2 = meshtastic.tcp_interface.TCPInterface(hostname2)
elif interface2_type == 'ble':
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
print(f"{log_timestamp()} System: Interface2 Opened!")
logger.debug(f"System: Interface2 Opened!")
retry_int2 = False
except Exception as e:
print(f"{log_timestamp()} System: Error 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
logger.error(f"System: opening interface2: {e}")
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"{log_timestamp()} System: watchdog running\r", end="")
#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:
print(f"{log_timestamp()} System: Error communicating with interface1, trying to reconnect: {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)
except Exception as e:
print(f"{log_timestamp()} System: Error retrying interface1: {e}")
logger.error(f"System: retrying interface1: {e}")
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:
print(f"{log_timestamp()} System: Error communicating with interface2, trying to reconnect: {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:
print(f"{log_timestamp()} System: Error retrying interface2: {e}")
logger.error(f"System: retrying interface2: {e}")

177
modules/wx_meteo.py Normal file
View File

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

View File

@@ -5,59 +5,112 @@
import asyncio
import time # for sleep, get some when you can :)
from pubsub import pub # pip install pubsub
from modules.settings import *
from modules.log import *
from modules.system import *
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 "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + 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 "testing" in message.lower() or "test" in message.lower():
bot_response = "🏓Testing 1,2,3"
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."
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 700ms to avoid message collision from lora-ack
time.sleep(0.7)
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__
@@ -79,11 +132,19 @@ def onReceive(packet, interface):
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
rxNode = 2
if rxType == 'BLEInterface':
if interface1_type == 'ble':
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# Debug print the packet for debugging
#print(f"Packet Received\n {packet} \n END of packet \n")
message_from_id = 0
# check for a message packet and process it
snr = 0
rssi = 0
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
@@ -131,7 +192,7 @@ def onReceive(packet, interface):
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
# ignore help and welcome messages
print(f"{log_timestamp()} Got Own Welcome/Help header. Device:{rxNode} From: {get_name_from_number(message_from_id)}")
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
@@ -140,25 +201,29 @@ def onReceive(packet, interface):
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
else:
# message is on a channel
if messageTrap(message_string):
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
# message is for bot to respond to
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# or respond to channel message on the channel itself
if channel_number == publicChannel:
if channel_number == publicChannel and antiSpam:
# warning user spamming default channel
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
@@ -166,8 +231,8 @@ def onReceive(packet, interface):
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# ignore the message but add it to the message history and repeat it if enabled
# add the message to the message history but limit
# message is not for bot to respond to
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -178,40 +243,53 @@ def onReceive(packet, interface):
else:
msg_history.pop(0)
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
# check if repeater is enabled and the other interface is enabled
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
# repeat the message on the other device
# wait a 700ms to avoid message collision from lora-ack.
time.sleep(0.7)
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:
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 2)
elif rxNode == 2:
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
else:
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
except KeyError as e:
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
print("END of packet \n")
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')
msg = (f"{log_timestamp()} 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)}")
print (msg)
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:
msg = (f"{log_timestamp()} 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)}")
print (msg)
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if sentry_enabled:
logger.debug(f"System: Sentry Enabled")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
# here we go loopty loo
while True:

View File

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