forked from iarv/meshing-around
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
979f197476 | ||
|
|
1677b69363 | ||
|
|
d627f694df | ||
|
|
4c52cba21f | ||
|
|
597fdd1695 | ||
|
|
9031704b9b | ||
|
|
510a5c5007 | ||
|
|
469e76c50b | ||
|
|
f6c6c58c17 | ||
|
|
e546866f78 | ||
|
|
081566b5d9 | ||
|
|
ec078666ae | ||
|
|
1ce394c7a1 | ||
|
|
2fc3930b43 | ||
|
|
9fa9da5e74 | ||
|
|
d6ad0b5e94 | ||
|
|
15dc50804f | ||
|
|
63c3e35064 | ||
|
|
297930c4d1 | ||
|
|
098c344047 | ||
|
|
4f74677d14 | ||
|
|
0869b19408 | ||
|
|
9b02611700 | ||
|
|
5daa71e6c1 | ||
|
|
aa5f2f66f8 | ||
|
|
92d04f81c3 | ||
|
|
5d53db4211 | ||
|
|
eb3bbdd3c5 | ||
|
|
1ac816ca37 | ||
|
|
33cf18cde5 | ||
|
|
0c0d53dd78 | ||
|
|
1959ee7560 | ||
|
|
ee13401b5a | ||
|
|
78b1cf4af5 | ||
|
|
0599260e31 | ||
|
|
08dd921088 | ||
|
|
e66e938d7d | ||
|
|
b5b7d2a9d2 | ||
|
|
46298d555b | ||
|
|
8fb34b5fde | ||
|
|
28f8986837 | ||
|
|
e968173f61 | ||
|
|
f703a8868b | ||
|
|
0a29e5f156 | ||
|
|
c5c28ee042 | ||
|
|
44ca43399d | ||
|
|
13a47d822d | ||
|
|
5621cd90bb | ||
|
|
9f7055ffd2 | ||
|
|
37a9fc2eb0 | ||
|
|
923325874c | ||
|
|
7ca0c4d744 | ||
|
|
a584a71429 | ||
|
|
70f47635b4 | ||
|
|
8e35d77e07 | ||
|
|
7024f2d472 | ||
|
|
7e2dd4c7ff | ||
|
|
f20d83ca8c | ||
|
|
f31f920137 | ||
|
|
0f428438a3 | ||
|
|
b7882b0322 | ||
|
|
3a417a9281 | ||
|
|
748085c2be | ||
|
|
6a3f56f95f | ||
|
|
f6d6fb7185 | ||
|
|
7865263c1c | ||
|
|
2cf51d5a09 | ||
|
|
f993be950f | ||
|
|
52c4c49bab | ||
|
|
60fdc7b7ea | ||
|
|
a330cff3e5 | ||
|
|
9ffbac7420 | ||
|
|
7909707894 | ||
|
|
8d8014b157 | ||
|
|
a459b7a393 | ||
|
|
7d405dc0c2 | ||
|
|
3decf8749b | ||
|
|
ba6869ec76 | ||
|
|
33cb70ea17 | ||
|
|
69f1b7471f | ||
|
|
76a7d1dba7 | ||
|
|
9f0d3c9d3b | ||
|
|
ff6292160f | ||
|
|
52dcb7972f | ||
|
|
10e2b0ee59 | ||
|
|
473eccbdea | ||
|
|
f6b2e0a506 | ||
|
|
22e16db1f2 | ||
|
|
2c71ca9b8a | ||
|
|
023189bca9 | ||
|
|
8447985b98 | ||
|
|
ad123dc93c | ||
|
|
22983133ee | ||
|
|
60c4a885fd | ||
|
|
95d6d7b7d5 | ||
|
|
37a86b7e2b | ||
|
|
c4ef1251c9 | ||
|
|
9d7e42aa60 | ||
|
|
8536e354ad | ||
|
|
e3faf676cd | ||
|
|
630e016805 | ||
|
|
23b8b8135c | ||
|
|
7f0b4c079a | ||
|
|
47649cdedc | ||
|
|
7915798ca2 | ||
|
|
86cd88910a | ||
|
|
229ccc75f0 | ||
|
|
6f3e3a7957 | ||
|
|
1f1996b909 | ||
|
|
c2069da919 | ||
|
|
458957ddfb | ||
|
|
95c266fbf3 | ||
|
|
4857940165 | ||
|
|
4c780d09e7 | ||
|
|
d616867cd1 | ||
|
|
909c4ad3bc | ||
|
|
44eff643a9 | ||
|
|
a223e57690 | ||
|
|
69bf2d7081 | ||
|
|
c64644a331 | ||
|
|
e8b82ca687 | ||
|
|
47bd8d1d26 | ||
|
|
a6e88a63d5 | ||
|
|
e6be9a7d13 | ||
|
|
8e34925af7 | ||
|
|
1ec6cefc16 | ||
|
|
4a4c5c3e0f | ||
|
|
19e6a38355 | ||
|
|
066f451a4d | ||
|
|
c50776b991 | ||
|
|
8daa9f71e2 | ||
|
|
340cff5e5b | ||
|
|
1747125ea7 | ||
|
|
6ce650dc15 | ||
|
|
d2b303b47c | ||
|
|
74c5bfa64b | ||
|
|
f826c0e4bb | ||
|
|
b8fc3c6c37 | ||
|
|
22b8c8a62e | ||
|
|
f7ad83d2b5 | ||
|
|
fa8b5d6b71 | ||
|
|
036bff1489 | ||
|
|
fe1854f2d8 | ||
|
|
df9a34dc16 | ||
|
|
e762ea4b90 | ||
|
|
3b725837ac | ||
|
|
23efd8e5d8 | ||
|
|
b61463f570 | ||
|
|
8339233459 | ||
|
|
df68111f0c | ||
|
|
b73ad38156 | ||
|
|
2b7d1ed09f | ||
|
|
f1ef5fa787 | ||
|
|
ec14e07513 | ||
|
|
efdd5fab66 | ||
|
|
4fa114a3f2 | ||
|
|
ab64ff14b1 | ||
|
|
65609c5822 | ||
|
|
bdd41c0434 | ||
|
|
80da793c8d | ||
|
|
ba6c296b14 | ||
|
|
9ae95752ad | ||
|
|
9ba430c53c | ||
|
|
9e605a2717 | ||
|
|
aeab22010f | ||
|
|
2d20f4479c | ||
|
|
6546679def |
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3.10-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
COPY config.ini /app/config.ini
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
110
README.md
110
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
## 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: ` will search wikipedia, return the first few sentances of first result if a match `wiki: lora radio`
|
||||
- `askai` and `ask:` will ask Ollama LLM AI for a response `askai what temp do I cook chicken`
|
||||
- `messages` Replay the last messages heard, like Store and Forward
|
||||
- `motd` or to set the message `motd $New Message Of the day`
|
||||
- `lheard` returns the last 5 heard nodes with SNR, can also use `sitrep`
|
||||
@@ -43,19 +51,26 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
|
||||
Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
|
||||
|
||||
## Hardware
|
||||
The project is written on Linux on a Pi and should work anywhere meshtastic Python modules will function, with any supported meshtastic hardware. While BLE and TCP will work, they are not as reliable as serial connections.
|
||||
- Firmware 2.3.14/15 could also have an issue with connectivity with slower devices.
|
||||
The project is written on Linux on a Pi and should work anywhere [Meshtastic](https://meshtastic.org/docs/software/python/cli/) Python modules will function, with any supported [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. While BLE and TCP will work, they are not as reliable as serial connections.
|
||||
|
||||
## Install
|
||||
Clone the project with `git clone https://github.com/spudgunman/meshing-around`
|
||||
code is under a lot of development, so check back often with `git pull`
|
||||
Copy [config.template](config.template) to `config.ini` and edit for your needs.
|
||||
- Optionally
|
||||
`pip install -r requirements.txt`
|
||||
|
||||
Optionally:
|
||||
- `install.sh` will automate optional venv and requirements installation.
|
||||
- `launch.sh` will activate and launch the app in the venv if built.
|
||||
|
||||
For Docker:
|
||||
Check you have serial port properly shared and the GPU if using LLM with [NVidia](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html)
|
||||
- `git clone https://github.com/spudgunman/meshing-around`
|
||||
- `cd meshing-around && docker build -t meshing-around`
|
||||
- `docker run meshing-around`
|
||||
|
||||
### Configurations
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect tcp if possible.
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect tcp if possible. To get BLE mac `meshtastic --ble-scan` **NOTE** I have only tested with a single BLE device and the code is written to only have one interface be a BLE port
|
||||
|
||||
```
|
||||
#config.ini
|
||||
@@ -82,7 +97,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 +115,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 +144,8 @@ A module allowing a Hamlib compatible radio to connect to the bot, when function
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
# channel to brodcast to can be 2,3
|
||||
sigWatchBrodcastCh = 2
|
||||
# channel to broadcast to can be 2,3
|
||||
sigWatchBroadcastCh = 2
|
||||
# minimum SNR as reported by radio via hamlib
|
||||
signalDetectionThreshold = -10
|
||||
# hold time for high SNR
|
||||
@@ -137,8 +154,54 @@ signalHoldTime = 10
|
||||
signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
Ollama Settings, for Ollama to work the command line `ollama run 'model'` needs to work properly. Check that you have enough RAM and your GPU are working as expected. The default model for this project, is set to `gemma2:2b` (run `ollama pull gemma2:2b` on command line, to download and setup) however I have found gemma2:2b to be lighter, faster and seems better overall vs llama3,1 (`olamma pull llama3.1`)
|
||||
- From the command terminal of your system with mesh-bot, download the default model for mesh-bot which is currently `ollama pull gemma2:2b`
|
||||
|
||||
Enable History, set via code readme Ollama Config in [Settings](https://github.com/SpudGunMan/meshing-around?tab=readme-ov-file#configurations) and [llm.py](https://github.com/SpudGunMan/meshing-around/blob/eb3bbdd3c5e0f16fe3c465bea30c781bd132d2d3/modules/llm.py#L12)
|
||||
|
||||
```
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = True
|
||||
# Ollama model to use (defaults to llama3.1)
|
||||
ollamaModel = gemma2:2b
|
||||
```
|
||||
|
||||
also see llm.py for changing the defaults of
|
||||
```
|
||||
# LLM System Variables
|
||||
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
|
||||
```
|
||||
|
||||
Logging messages to disk or Syslog to disk uses the python native logging function. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level.
|
||||
```
|
||||
[general]
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = True
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = True
|
||||
```
|
||||
Example to log to disk only INFO and higher (ignore DEBUG)
|
||||
```
|
||||
*log.py
|
||||
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
|
||||
```
|
||||
|
||||
The Scheduler is enabled in the [settings.py](modules/settings.py) by setting `scheduler_enabled = True` the actions and settings are via code only at this time. see [mesh_bot.py](mesh_bot.py) around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit schedule its most flexible to edit raw code right now. See https://schedule.readthedocs.io/en/stable/ for more.
|
||||
|
||||
```
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
# requirements
|
||||
can also be installed with `pip install -r requirements.txt`
|
||||
Python 3.10 minimally is needed, developed on latest release.
|
||||
|
||||
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
|
||||
|
||||
```
|
||||
pip install meshtastic
|
||||
@@ -154,6 +217,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 +226,22 @@ pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
The following is for the Ollama LLM
|
||||
```
|
||||
pip install langchain
|
||||
pip install langchain-ollama
|
||||
pip install ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
|
||||
|
||||
# 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!
|
||||
|
||||
|
||||
@@ -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
6
entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Substitute environment variables in the config file
|
||||
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
|
||||
exec python /app/mesh_bot.py
|
||||
@@ -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)
|
||||
|
||||
38
install.sh
38
install.sh
@@ -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
|
||||
|
||||
525
mesh_bot.py
525
mesh_bot.py
@@ -10,173 +10,296 @@ 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),
|
||||
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
"joke": tell_joke,
|
||||
"bbslist": bbs_list_messages,
|
||||
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
|
||||
"messages": lambda: handle_messages(deviceID, channel_number, msg_history, publicChannel),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"hfcond": hf_band_conditions,
|
||||
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
|
||||
"lheard": lambda: handle_lheard(),
|
||||
"sitrep": lambda: handle_lheard(),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
|
||||
"ack": lambda: handle_ack(hop, snr, rssi),
|
||||
"testing": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_testing(hop, snr, rssi),
|
||||
}
|
||||
cmds = [] # list to hold the commands found in the message
|
||||
for key in command_handler:
|
||||
if key in message_lower.split(' '):
|
||||
cmds.append({'cmd': key, 'index': message_lower.index(key)})
|
||||
|
||||
if len(cmds) > 0:
|
||||
# sort the commands by index value
|
||||
cmds = sorted(cmds, key=lambda k: k['index'])
|
||||
logger.debug(f"System: Bot detected Commands:{cmds}")
|
||||
# run the first command after sorting
|
||||
bot_response = command_handler[cmds[0]['cmd']]()
|
||||
|
||||
elif "bbsread" in message.lower():
|
||||
# Check if the user added a message number to the message
|
||||
if "#" in message 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, llmLocationTable
|
||||
|
||||
if location_enabled:
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
# if message_from_id is is the llmLocationTable use the location from the table to save on API calls
|
||||
if message_from_id in llmLocationTable:
|
||||
location = llmLocationTable[message_from_id]
|
||||
else:
|
||||
location_name = where_am_i(str(location[0]), str(location[1]), short = True)
|
||||
llmLocationTable.append({message_from_id: location_name})
|
||||
|
||||
if NO_DATA_NOGPS in location_name:
|
||||
location_name = "no location provided "
|
||||
else:
|
||||
location_name = "no location provided "
|
||||
|
||||
if "ask:" in message.lower():
|
||||
user_input = message.split(":")[1]
|
||||
elif "askai" in message.lower():
|
||||
user_input = message.replace("askai", "")
|
||||
else:
|
||||
user_input = message
|
||||
user_input = user_input.strip()
|
||||
|
||||
if len(user_input) < 1:
|
||||
return "Please ask a question"
|
||||
|
||||
# information for the user on how long the query will take on average
|
||||
if llmRunCounter > 0:
|
||||
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
|
||||
if averageRuntime > 25:
|
||||
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
|
||||
if channel_number == publicChannel:
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
else:
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
else:
|
||||
msg = "Please wait, response could take 3+ minutes. Fund the SysOp's GPU budget!"
|
||||
if channel_number == publicChannel:
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
else:
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
|
||||
start = time.time()
|
||||
|
||||
#response = asyncio.run(llm_query(user_input, message_from_id))
|
||||
response = llm_query(user_input, message_from_id, location_name)
|
||||
|
||||
# handle the runtime counter
|
||||
end = time.time()
|
||||
llmRunCounter += 1
|
||||
llmTotalRuntime.append(end - start)
|
||||
|
||||
return response
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
|
||||
logger.debug(f"System: Bot Returning Open-Meteo API for weather imperial")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]))
|
||||
elif use_meteo_wxApi:
|
||||
logger.debug(f"System: Bot Returning Open-Meteo API for weather metric")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
|
||||
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
|
||||
logger.debug(f"System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_weather(str(location[0]), str(location[1]), 1)
|
||||
else:
|
||||
logger.debug(f"System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_weather(str(location[0]), str(location[1]))
|
||||
return weather
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
if "$" in message and not "example:" in message:
|
||||
subject = message.split("$")[1].split("#")[0]
|
||||
subject = subject.rstrip()
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
body = body.rstrip()
|
||||
logger.info(f"System: BBS Post: {subject} Body: {body}")
|
||||
return bbs_post_message(subject, body, message_from_id)
|
||||
elif not "example:" in message:
|
||||
return "example: bbspost $subject #message"
|
||||
elif "@" in message and not "example:" in message:
|
||||
toNode = message.split("@")[1].split("#")[0]
|
||||
toNode = toNode.rstrip()
|
||||
if toNode.isalpha() or not toNode.isnumeric():
|
||||
toNode = get_num_from_short_name(toNode, deviceID)
|
||||
if toNode == 0:
|
||||
return "Node not found " + message.split("@")[1].split("#")[0]
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
return bbs_post_dm(toNode, body, message_from_id)
|
||||
else:
|
||||
return "example: bbspost @nodeNumber/ShortName #message"
|
||||
elif not "example:" in message:
|
||||
return "example: bbspost $subject #message, or bbspost @node #message"
|
||||
|
||||
def handle_bbsread(message):
|
||||
if "#" in message and not "example:" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
return bbs_read_message(messageID)
|
||||
elif not "example:" in message:
|
||||
return "Please add a message number example: bbsread #14"
|
||||
|
||||
def handle_bbsdelete(message, message_from_id):
|
||||
if "#" in message and not "example:" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
return bbs_delete_message(messageID, message_from_id)
|
||||
elif not "example:" in message:
|
||||
return "Please add a message number example: bbsdelete #14"
|
||||
|
||||
def handle_messages(deviceID, channel_number, msg_history, publicChannel):
|
||||
response = ""
|
||||
for msgH in msg_history:
|
||||
if msgH[4] == deviceID:
|
||||
if msgH[2] == channel_number or msgH[2] == publicChannel:
|
||||
response += f"\n{msgH[0]}: {msgH[1]}"
|
||||
if len(response) > 0:
|
||||
return "Message History:" + response
|
||||
else:
|
||||
return "No messages in history"
|
||||
|
||||
def handle_sun(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_lheard():
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
bot_response += "Ch Use: " + str(chutil1) + "%"
|
||||
if interface2_enabled:
|
||||
bot_response += " P2:" + str(chutil2) + "%"
|
||||
return bot_response
|
||||
|
||||
def handle_whereami(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return where_am_i(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_tide(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_tide(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_moon(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_moon(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
rxType = type(interface).__name__
|
||||
rxNode = 0
|
||||
#logger.debug(f"System: Packet Received on {rxType}")
|
||||
# Debug print the interface object
|
||||
#for item in interface.__dict__.items(): print (item)
|
||||
|
||||
@@ -194,6 +317,12 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# Debug print the packet for debugging
|
||||
#print(f"Packet Received\n {packet} \n END of packet \n")
|
||||
message_from_id = 0
|
||||
@@ -217,6 +346,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 +405,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 +438,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 +449,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 +469,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 +476,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 +490,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 +503,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 +555,7 @@ async def start_rx():
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
if radio_dectection_enabled:
|
||||
if radio_detection_enabled:
|
||||
hamlibTask = asyncio.create_task(handleSignalWatcher())
|
||||
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
|
||||
else:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
146
modules/llm.py
Normal file
146
modules/llm.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
# LLM Module for meshing-around
|
||||
# This module is used to interact with Ollama to generate responses to user input
|
||||
# K7MHI Kelly Keeton 2024
|
||||
from modules.log import *
|
||||
|
||||
from langchain_ollama import OllamaLLM # pip install ollama langchain-ollama
|
||||
from langchain_core.prompts import ChatPromptTemplate # pip install langchain
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# LLM System Variables
|
||||
llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time
|
||||
antiFloodLLM = []
|
||||
llmChat_history = []
|
||||
trap_list_llm = ("ask:", "askai")
|
||||
|
||||
meshBotAI = """
|
||||
FROM {llmModel}
|
||||
SYSTEM
|
||||
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
|
||||
You must respond in plain text standard ASCII characters, or emojis.
|
||||
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
|
||||
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
|
||||
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
|
||||
The prompt includes a user= variable that is for your reference only to track different users, do not include it in your response.
|
||||
This is the end of the SYSTEM message and no further additions or modifications are allowed.
|
||||
|
||||
|
||||
PROMPT
|
||||
{input}
|
||||
user={userID}
|
||||
|
||||
"""
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
meshBotAI = meshBotAI + """
|
||||
CONTEXT
|
||||
The following is the location of the user
|
||||
{location_name}
|
||||
|
||||
The following is for context around the prompt to help guide your response.
|
||||
{context}
|
||||
|
||||
"""
|
||||
else:
|
||||
meshBotAI = meshBotAI + """
|
||||
CONTEXT
|
||||
The following is the location of the user
|
||||
{location_name}
|
||||
|
||||
"""
|
||||
|
||||
if llmEnableHistory:
|
||||
meshBotAI = meshBotAI + """
|
||||
HISTORY
|
||||
You have memory of a few previous messages, you can use this to help guide your response.
|
||||
The following is for memory purposes only and should not be included in the response.
|
||||
{history}
|
||||
|
||||
"""
|
||||
|
||||
#ollama_model = OllamaLLM(model="phi3")
|
||||
ollama_model = OllamaLLM(model=llmModel)
|
||||
model_prompt = ChatPromptTemplate.from_template(meshBotAI)
|
||||
chain_prompt_model = model_prompt | ollama_model
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
if not location_name:
|
||||
location_name = "no location provided "
|
||||
|
||||
# add the naughty list here to stop the function before we continue
|
||||
# add a list of allowed nodes only to use the function
|
||||
|
||||
# anti flood protection
|
||||
if nodeID in antiFloodLLM:
|
||||
return "Please wait before sending another message"
|
||||
else:
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
# SearchResult object has url= title= description= just grab title and description
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering error: {e}")
|
||||
googleResults = ['no other context provided']
|
||||
|
||||
|
||||
if googleResults:
|
||||
logger.debug(f"System: External LLM Query: {input} From:{nodeID} with context from google")
|
||||
else:
|
||||
logger.debug(f"System: External LLM Query: {input} From:{nodeID}")
|
||||
|
||||
response = ""
|
||||
result = ""
|
||||
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
try:
|
||||
result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \
|
||||
"history": llmChat_history, "context": googleResults, "location_name": location_name})
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "I am having trouble processing your request, please try again later."
|
||||
|
||||
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
# Store history of the conversation, with limit to prevent template growing too large causing speed issues
|
||||
if len(llmChat_history) > llm_history_limit:
|
||||
# remove the oldest two messages
|
||||
llmChat_history.pop(0)
|
||||
llmChat_history.pop(1)
|
||||
inputWithUserID = input + f" user={nodeID}"
|
||||
llmChat_history.append(HumanMessage(content=inputWithUserID))
|
||||
llmChat_history.append(AIMessage(content=response))
|
||||
|
||||
# done with the query, remove the user from the anti flood list
|
||||
antiFloodLLM.remove(nodeID)
|
||||
|
||||
return response
|
||||
|
||||
# import subprocess
|
||||
# def get_ollama_cpu():
|
||||
# try:
|
||||
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
|
||||
# if "GPU" in psOutput.stdout:
|
||||
# logger.debug(f"System: Ollama process with GPU")
|
||||
# else:
|
||||
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
|
||||
# except Exception as e:
|
||||
# logger.debug(f"System: Ollama process not found, {e}")
|
||||
# return False
|
||||
@@ -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
|
||||
@@ -11,18 +11,25 @@ from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
|
||||
|
||||
def where_am_i(lat=0, lon=0):
|
||||
def where_am_i(lat=0, lon=0, short=False):
|
||||
whereIam = ""
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
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
|
||||
geolocator = Nominatim(user_agent="mesh-bot")
|
||||
|
||||
# Nomatim API call to get address
|
||||
if short:
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = ['city', 'state', 'county', 'country']
|
||||
whereIam = f"City: {address.get('city', '')} State: {address.get('state', '')} County: {address.get('county', '')} Country: {address.get('country', '')}"
|
||||
return whereIam
|
||||
|
||||
if float(lat) == latitudeValue and float(lon) == longitudeValue:
|
||||
# redacted address when no GPS and using default location
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
@@ -30,19 +37,18 @@ def where_am_i(lat=0, lon=0):
|
||||
address_components = ['city', 'state', 'postcode', 'county', 'country']
|
||||
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
|
||||
whereIam += " Grid: " + grid
|
||||
return whereIam
|
||||
else:
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = ['house_number', 'road', 'city', 'state', 'postcode', 'county', 'country']
|
||||
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
|
||||
whereIam += " Grid: " + grid
|
||||
return whereIam
|
||||
return whereIam
|
||||
|
||||
def get_tide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, 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 +198,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 +217,15 @@ def getWeatherAlerts(lat=0, lon=0):
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
#logger.debug("Location:Fetching weather alerts from NOAA for " + str(lat) + ", " + str(lon))
|
||||
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.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 +257,20 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.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 = ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Settings for MeshBot and PongBot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
import configparser
|
||||
|
||||
# messages
|
||||
@@ -22,6 +24,11 @@ max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
scheduler_enabled = False # enable the scheduler currently config via code only
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
llmLocationTable = []
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -40,6 +47,26 @@ if config.sections() == []:
|
||||
config.write(open(config_file, 'w'))
|
||||
print (f"System: Config file created, check {config_file} or review the config.template")
|
||||
|
||||
if 'sentry' not in config:
|
||||
config['Sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'location' not in config:
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'bbs' not in config:
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'repeater' not in config:
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'radioMon' not in config:
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
@@ -58,39 +85,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('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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 + ", askai"
|
||||
|
||||
# Scheduled Broadcast Configuration
|
||||
if scheduler_enabled:
|
||||
import schedule # pip install schedule
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
|
||||
# Sentry Configuration
|
||||
if sentry_enabled:
|
||||
from math import sqrt
|
||||
import geopy.distance # pip install geopy
|
||||
@@ -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: Initializing Interface1")
|
||||
if interface1_type == 'serial':
|
||||
interface1 = meshtastic.serial_interface.SerialInterface(port1)
|
||||
elif interface1_type == 'tcp':
|
||||
@@ -81,11 +114,12 @@ try:
|
||||
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. Initalizing Interface1 {e}")
|
||||
logger.critical(f"System: script abort. Initializing Interface1 {e}")
|
||||
exit()
|
||||
|
||||
# Interface2 Configuration
|
||||
if interface2_enabled:
|
||||
logger.debug(f"System: Initializing Interface2")
|
||||
try:
|
||||
if interface2_type == 'serial':
|
||||
interface2 = meshtastic.serial_interface.SerialInterface(port2)
|
||||
@@ -97,7 +131,7 @@ if interface2_enabled:
|
||||
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. Initalizing Interface2 {e}")
|
||||
logger.critical(f"System: script abort. Initializing Interface2 {e}")
|
||||
exit()
|
||||
|
||||
#Get the node number of the device, check if the device is connected
|
||||
@@ -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,9 +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()
|
||||
#if "device_state_version:" not in meta:
|
||||
print(f"System: if you see this upgrade python to >3.4")
|
||||
except Exception as e:
|
||||
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
|
||||
retry_int1 = True
|
||||
@@ -615,6 +691,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 +708,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 +725,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
|
||||
|
||||
175
pong_bot.py
175
pong_bot.py
@@ -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:
|
||||
|
||||
@@ -10,4 +10,10 @@ dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
langchain
|
||||
langchain-ollama
|
||||
ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user