Compare commits

...

140 Commits
RC9 ... v1.1.1

Author SHA1 Message Date
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
16 changed files with 945 additions and 385 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"]

View File

@@ -1,24 +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` 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`.
The bot will report on anyone who is getting close to the device if in a remote location.
Look up data using wiki results or interact with [Ollama](https://ollama.com) LLM AI see the [OllamaDocs](https://github.com/ollama/ollama/tree/main/docs) If Ollama is enabled you can DM the bot directly. The default model for mesh-bot which is currently `gemma2:2b`
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location.
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
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
@@ -31,9 +37,11 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
- 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, (wxc is metric value), NOAA or Open Meteo for weather forcasting.
- `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: ` search wikipedia, return the first few sentances of first result if a match `wiki: lora radio`
- `ask: ` ask Ollama LLM AI for a response `ask: 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`
@@ -43,19 +51,25 @@ 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:
- `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
@@ -82,7 +96,7 @@ 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 forcasting 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.
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
@@ -100,11 +114,13 @@ enabled = False
DadJokes = False
StoreForward = False
```
Sentry Bot detects anyone comeing close to the bot-node
Sentry Bot detects anyone coming close to the bot-node
```
# detect anyone close to the bot
SentryEnabled = True
# holdoff time multiplied by minutes(20) of the watchdog
# 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
@@ -127,8 +143,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
@@ -137,8 +153,43 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
```
Ollama Settings, for Ollama to work the command line `ollama run 'model'` needs to work properly. Check that you have enough RAM and your GPU are working as expected. The default model for this project, is set to `gemma2:2b` (run `ollama pull gemma2:2b` on command line, to download and setup) 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 ollama LLM see more at https://ollama.com
ollama = True
# Ollama model to use (defaults to llama3.1)
ollamaModel = gemma2:2b
```
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
@@ -154,6 +205,8 @@ 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
```
@@ -161,15 +214,21 @@ 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
```
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
# 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,23 +32,35 @@ 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
# holdoff time multiplied by minutes(20) of the watchdog
SentryChannel = 9
# channel to send a message to when the watchdog is triggered
SentryHoldoff = 2
SentryChannel = 9
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
@@ -68,15 +80,11 @@ lon = -123.0
NOAAforecastDuration = 4
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA usefull for non US locations
# 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
# solar module
[solar]
enabled = True
# repeater module
[repeater]
enabled = False
@@ -90,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

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

@@ -10,173 +10,278 @@ from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
#Auto response to messages
bot_response = ""
if "ping" in message.lower():
#Check if the user added @foo to the message
if "@" in message:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
else:
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
else:
if hop == "Direct":
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓PONG, " + hop
elif "pong" in message.lower():
bot_response = "🏓PING!!"
elif "motd" in message.lower():
#check if the user wants to set the motd by using $
if "$" in message:
motd = message.split("$")[1]
global MOTD
MOTD = motd
bot_response = "MOTD Set to: " + MOTD
else:
bot_response = MOTD
elif "messages" in message.lower():
response = ""
for msgH in msg_history:
# check if the message is from the same interface
if msgH[4] == deviceID:
# check if the message is from the same channel
if msgH[2] == channel_number or msgH[2] == publicChannel:
# consider message safe to send
response += f"\n{msgH[0]}: {msgH[1]}"
if len(response) > 0:
bot_response = "Message History:" + response
else:
bot_response = "No messages in history"
elif "bbshelp" in message.lower():
bot_response = bbs_help()
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "sun" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
bot_response = get_sun(str(location[0]),str(location[1]))
elif "hfcond" in message.lower():
bot_response = hf_band_conditions()
elif "solar" in message.lower():
bot_response = drap_xray_conditions() + "\n" + solar_conditions()
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
elif "whereami" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
where = where_am_i(str(location[0]),str(location[1]))
bot_response = where
elif "tide" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
tide = get_tide(str(location[0]),str(location[1]))
bot_response = tide
elif "moon" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
moon = get_moon(str(location[0]),str(location[1]))
bot_response = moon
elif "wxalert" in message.lower() or "wxa" in message.lower():
if use_meteo_wxApi:
bot_response = "wxalert is not supported"
else:
location = get_node_location(message_from_id, deviceID)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxc" in message.lower() or "wx" in message.lower():
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in message.lower() 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 message.lower() 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]))
bot_response = weather
elif "joke" in message.lower():
bot_response = tell_joke()
elif "bbslist" in message.lower():
bot_response = bbs_list_messages()
elif "bbspost" in message.lower():
# Check if the user added a subject to the message
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
logger.info(f"System: BBS Post: {subject} Body: {body}")
bot_response = bbs_post_message(subject,body,message_from_id)
elif not "example:" in message:
bot_response = "example: bbspost $subject #message"
# Check if the user added a node number to the message
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
# if toNode is a string look for short name and convert to number
if toNode.isalpha() or not toNode.isnumeric():
toNode = get_num_from_short_name(toNode, deviceID)
if toNode == 0:
bot_response = "Node not found " + message.split("@")[1].split("#")[0]
return bot_response
else:
logger.debug(f"System: bbspost, name lookup found: {toNode}")
if "#" in message:
body = message.split("#")[1]
bot_response = bbs_post_dm(toNode, body, message_from_id)
else:
bot_response = "example: bbspost @nodeNumber/ShortName #message"
elif not "example:" in message:
bot_response = "example: bbspost $subject #message, or bbspost @node #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),
"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 and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_read_message(messageID)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsread #14"
elif "bbsdelete" in message.lower():
# Check if the user added a message number to the message
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsdelete #14"
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7)
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
if "ask:" in message.lower():
user_input = message.split(":")[1]
user_input = user_input.strip()
else:
user_input = message
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)
# 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)
@@ -194,6 +299,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
@@ -217,6 +328,8 @@ def onReceive(packet, interface):
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']
@@ -274,10 +387,16 @@ def onReceive(packet, interface):
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
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
@@ -301,7 +420,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -312,10 +431,15 @@ 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)}")
@@ -327,12 +451,6 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else:
# nothing to do for us
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
@@ -340,8 +458,13 @@ def onReceive(packet, interface):
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ", myNodeNum1)
logger.debug(f"System: LLM model {llmModel} loaded")
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
@@ -349,6 +472,8 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk")
if syslog_to_file:
logger.debug(f"System: Logging System Logs to disk")
if bbs_enabled:
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
if solar_conditions_enabled:
@@ -360,16 +485,48 @@ async def start_rx():
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")
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
#
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
# here we go loopty loo
while True:
@@ -380,7 +537,7 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if radio_dectection_enabled:
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:

View File

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

81
modules/llm.py Normal file
View File

@@ -0,0 +1,81 @@
#!/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
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import AIMessage, HumanMessage
# LLM System Variables
llmEnableHistory = False
llm_history_limit = 6 # limit the history to 3 messages (come in pairs)
antiFloodLLM = []
llmChat_history = []
trap_list_llm = ("ask:",)
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 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):
global antiFloodLLM, llmChat_history
# 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)
response = ""
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, "history": llmChat_history})
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
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

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
@@ -15,8 +15,8 @@ def where_am_i(lat=0, lon=0):
whereIam = ""
grid = mh.to_maiden(float(lat), float(lon))
if float(lat) == 0 and float(lon) == 0:
logger.error("Location: No GPS data, cant find where you are")
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
@@ -42,7 +42,7 @@ def where_am_i(lat=0, lon=0):
def get_tide(lat=0, lon=0):
station_id = ""
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, cant find where you are for tide")
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:
@@ -192,7 +192,9 @@ def abbreviate_weather(row):
"West": "W",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms"
"thunderstorms": "t-storms",
"quarters": "qtrs",
"quarter": "qtr"
}
line = row
@@ -209,14 +211,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.error("Location:Error fetching weather alerts from NOAA")
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts from NOAA")
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
alerts = ""
@@ -248,19 +251,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.error("Location:No GPS data, cant find where you are for weather alerts")
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.error("Location:Error fetching weather alerts detailed from NOAA")
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts detailed from NOAA")
logger.warning("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA
alerts = ""

View File

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

View File

@@ -1,3 +1,5 @@
# Settings for MeshBot and PongBot
# 2024 Kelly Keeton K7MHI
import configparser
# messages
@@ -22,6 +24,10 @@ max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
retry_int1 = False
retry_int2 = False
scheduler_enabled = False # enable the scheduler currently config via code only
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
llmRunCounter = 0
llmTotalRuntime = []
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -40,6 +46,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,39 +84,49 @@ 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(f'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)
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
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)
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)
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
sentry_enabled = config['general'].getboolean('SentryEnabled', True) # default True
secure_channel = config['general'].getint('SentryChannel', 2) # default 2
sentry_holdoff = config['general'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['general'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['general'].getint('SentryRadius', 100) # default 100 meters
config['general'].get('motd', MOTD)
urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds
forecastDuration = config['general'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['general'].getint('NOAAalertCount', 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

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

View File

@@ -1,4 +1,4 @@
# helper functions for system related tasks
# helper functions and init for system related tasks
# K7MHI Kelly Keeton 2024
import meshtastic.serial_interface #pip install meshtastic
@@ -6,6 +6,7 @@ import meshtastic.tcp_interface
import meshtastic.ble_interface
import time
import asyncio
import contextlib # for suppressing output on watchdog
from modules.log import *
# Global Variables
@@ -20,11 +21,18 @@ 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
@@ -56,6 +64,25 @@ 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 + ", ask:"
# 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
@@ -66,11 +93,17 @@ if store_forward_enabled:
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: Initalizing Interface1")
if interface1_type == 'serial':
interface1 = meshtastic.serial_interface.SerialInterface(port1)
elif interface1_type == 'tcp':
@@ -86,6 +119,7 @@ except Exception as e:
# Interface2 Configuration
if interface2_enabled:
logger.debug(f"System: Initalizing Interface2")
try:
if interface2_type == 'serial':
interface2 = meshtastic.serial_interface.SerialInterface(port2)
@@ -118,6 +152,8 @@ if interface2_enabled:
else:
myNodeNum2 = 777
# functions below
def decimal_to_hex(decimal_number):
return f"!{decimal_number:08x}"
@@ -159,12 +195,33 @@ def get_num_from_short_name(short_name, nodeInt=1):
logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}")
if nodeInt == 1:
for node in interface1.nodes.values():
if str(short_name.lower()) == node['user']['shortName'].lower():
#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 str(short_name.lower()) == node['user']['shortName'].lower():
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):
@@ -173,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():
@@ -199,10 +257,8 @@ def get_node_list(nodeInt=1):
node_name = get_name_from_number(node['num'], 'long', nodeInt)
snr = node.get('snr', 0)
# issue where lastHeard is not always present, also had issues with None
# issue where lastHeard is not always present
last_heard = node.get('lastHeard', 0)
if last_heard is None:
last_heard = 0
# make a list of nodes with last heard time and SNR
item = (node_name, last_heard, snr)
@@ -213,13 +269,15 @@ def get_node_list(nodeInt=1):
try:
#print (f"Node List: {node_list1[:5]}\n")
node_list1.sort(key=lambda x: x[1], reverse=True)
node_list1.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
#print (f"Node List: {node_list1[:5]}\n")
node_list2.sort(key=lambda x: x[1], reverse=True)
if interface2_enabled:
node_list2.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
except Exception as e:
logger.error(f"System: Error sorting node list: {e}")
#print (f"Node List1: {node_list1[:5]}\n")
#print (f"Node List2: {node_list2[:5]}\n")
logger.debug(f"Node List1: {node_list1[:5]}\n")
if interface2_enabled:
logger.debug(f"Node List2: {node_list2[:5]}\n")
node_list = ERROR_FETCHING_DATA
try:
@@ -243,6 +301,7 @@ def get_node_location(number, nodeInt=1, channel=0):
latitude = latitudeValue
longitude = longitudeValue
position = [latitudeValue,longitudeValue]
lastheard = 0
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
@@ -258,17 +317,15 @@ def get_node_location(number, nodeInt=1, channel=0):
return position
else:
logger.warning(f"System: No location data for {number} using default location")
# request location data
try:
logger.debug(f"System: Requesting location data for {number}")
if nodeInt == 1:
interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
if nodeInt == 2:
interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
except Exception as e:
logger.error(f"System: Error requesting location data for {number}. Error: {e}")
# try:
# logger.debug(f"System: Requesting location data for {number}")
# if nodeInt == 1:
# interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
# if nodeInt == 2:
# interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
return position
else:
logger.warning(f"System: No nodes found")
@@ -292,9 +349,11 @@ def get_node_location(number, nodeInt=1, channel=0):
else:
logger.warning(f"System: No nodes found")
return position
return position
def get_closest_nodes(nodeInt=1,returnCount=3):
node_list = []
if nodeInt == 1:
if interface1.nodes:
for node in interface1.nodes.values():
@@ -304,13 +363,18 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
# set radius around BOT position
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
#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})
# calculate distance to node and report
except Exception as e:
pass
@@ -330,6 +394,7 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
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():
@@ -339,20 +404,23 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
latitude = node['position']['latitude']
longitude = node['position']['longitude']
# set radius around BOT position
distance = geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m
#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})
# calculate distance to node and report
except Exception as e:
pass
#sort by distance closest to lattitudeValue, longitudeValue
node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
# 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:
@@ -360,7 +428,7 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
return ERROR_FETCHING_DATA
def send_message(message, ch, nodeid=0, nodeInt=1):
if message == "":
if message == "" or message == None or len(message) == 0:
return
# if message over MESSAGE_CHUNK_SIZE characters, split it into multiple messages
if len(message) > MESSAGE_CHUNK_SIZE:
@@ -375,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 = ''
@@ -386,10 +454,11 @@ 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
# 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)
@@ -428,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
@@ -448,7 +533,7 @@ def messageTrap(msg):
def exit_handler():
# Close the interface and save the BBS messages
logger.debug(f"\nSystem: Closing Autoresponder\n")
logger.debug(f"System: Closing Autoresponder")
try:
interface1.close()
logger.debug(f"System: Interface1 Closed")
@@ -466,8 +551,14 @@ def exit_handler():
asyncLoop.close()
exit (0)
async def BroadcastScheduler():
# handle schedule checks for the broadcast of messages
while True:
schedule.run_pending()
await asyncio.sleep(1)
async def handleSignalWatcher():
global lastHamLibAlert, antiSpam, sigWatchBrodcastCh
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
# monitor rigctld for signal strength and frequency
while True:
msg = await signalWatcher()
@@ -478,21 +569,21 @@ async def handleSignalWatcher():
if time.time() - lastHamLibAlert > 60:
lastHamLibAlert = time.time()
# if sigWatchBrodcastCh list contains multiple channels, broadcast to all
if type(sigWatchBrodcastCh) is list:
for ch in sigWatchBrodcastCh:
if type(sigWatchBroadcastCh) is list:
for ch in sigWatchBroadcastCh:
if antiSpam and ch != publicChannel:
send_message(msg, int(ch), 0, 1)
if interface2_enabled:
send_message(msg, int(ch), 0, 2)
else:
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
else:
if antiSpam and sigWatchBrodcastCh != publicChannel:
send_message(msg, int(sigWatchBrodcastCh), 0, 1)
if antiSpam and sigWatchBroadcastCh != publicChannel:
send_message(msg, int(sigWatchBroadcastCh), 0, 1)
if interface2_enabled:
send_message(msg, int(sigWatchBrodcastCh), 0, 2)
send_message(msg, int(sigWatchBroadcastCh), 0, 2)
else:
logger.error(f"System: antiSpam prevented Alert from Hamlib {msg}")
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
await asyncio.sleep(1)
pass
@@ -560,22 +651,6 @@ async def retry_interface(nodeID=1):
except Exception as e:
logger.error(f"System: opening interface2: {e}")
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
from contextlib import contextmanager
import os
import sys
@contextmanager
def suppress_stdout():
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
async def watchdog():
global retry_int1, retry_int2
if sentry_enabled:
@@ -591,8 +666,10 @@ async def watchdog():
#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()
print(f"System: if you see this upgrade python to >3.4")
#if "device_state_version:" not in meta:
except Exception as e:
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
@@ -615,6 +692,9 @@ async def watchdog():
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:
@@ -629,8 +709,9 @@ async def watchdog():
if interface2_enabled:
if interface2 is not None and not retry_int2:
try:
with suppress_stdout():
with contextlib.redirect_stdout(None):
interface2.localNode.getMetadata()
print(f"System: if you see this upgrade python to >3.4")
except Exception as e:
logger.error(f"System: communicating with interface2, trying to reconnect: {e}")
retry_int2 = True
@@ -645,12 +726,15 @@ async def watchdog():
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'])
enemySpotted += f" at {closest_nodes1[0]['distance']}m"
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

View File

@@ -10,57 +10,107 @@ 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 "pong" in message.lower():
bot_response = "🏓Ping!!"
elif "motd" in message.lower():
# check if the user wants to set the motd by using $
if "$" in message:
motd = message.split("$")[1]
global MOTD
MOTD = motd
bot_response = "MOTD Set to: " + MOTD
else:
bot_response = MOTD
elif "cmd" in message.lower() or "cmd?" in message.lower():
bot_response = help_message
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
message_lower = message.lower()
bot_response = "I'm sorry, I'm afraid I can't do that."
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__
@@ -82,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']
@@ -174,7 +232,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# ignore the message but add it to the message history and repeat it if enabled
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
@@ -185,14 +243,19 @@ def onReceive(packet, interface):
else:
msg_history.pop(0)
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
# check if repeater is enabled and the other interface is enabled
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
if repeater_enabled and interface2_enabled:
# repeat the message on the other device
# wait a 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:
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
@@ -200,11 +263,6 @@ def onReceive(packet, interface):
elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1)
else:
# nothing to do for us
logger.info(f"Ignoring Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
print(packet) # print the packet for debugging
@@ -214,6 +272,7 @@ async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
if interface2_enabled:
@@ -229,8 +288,8 @@ async def start_rx():
logger.debug(f"System: Respond by DM only")
if repeater_enabled and interface2_enabled:
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_dectection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
# here we go loopty loo
while True:

View File

@@ -10,4 +10,10 @@ dadjokes
openmeteo_requests
retry_requests
numpy
geopy
geopy
schedule
wikipedia
langchain
langchain-ollama
ollama