forked from iarv/meshing-around
Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a9fc2eb0 | ||
|
|
923325874c | ||
|
|
7ca0c4d744 | ||
|
|
a584a71429 | ||
|
|
70f47635b4 | ||
|
|
8e35d77e07 | ||
|
|
7024f2d472 | ||
|
|
7e2dd4c7ff | ||
|
|
f20d83ca8c | ||
|
|
f31f920137 | ||
|
|
0f428438a3 | ||
|
|
b7882b0322 | ||
|
|
3a417a9281 | ||
|
|
748085c2be | ||
|
|
6a3f56f95f | ||
|
|
f6d6fb7185 | ||
|
|
7865263c1c | ||
|
|
2cf51d5a09 | ||
|
|
f993be950f | ||
|
|
52c4c49bab | ||
|
|
60fdc7b7ea | ||
|
|
a330cff3e5 | ||
|
|
9ffbac7420 | ||
|
|
7909707894 | ||
|
|
8d8014b157 | ||
|
|
a459b7a393 | ||
|
|
7d405dc0c2 | ||
|
|
3decf8749b | ||
|
|
ba6869ec76 | ||
|
|
33cb70ea17 | ||
|
|
69f1b7471f | ||
|
|
76a7d1dba7 | ||
|
|
9f0d3c9d3b | ||
|
|
ff6292160f | ||
|
|
52dcb7972f | ||
|
|
10e2b0ee59 | ||
|
|
473eccbdea | ||
|
|
f6b2e0a506 | ||
|
|
22e16db1f2 | ||
|
|
2c71ca9b8a | ||
|
|
023189bca9 | ||
|
|
8447985b98 | ||
|
|
ad123dc93c | ||
|
|
22983133ee | ||
|
|
60c4a885fd | ||
|
|
95d6d7b7d5 | ||
|
|
37a86b7e2b | ||
|
|
c4ef1251c9 | ||
|
|
9d7e42aa60 | ||
|
|
8536e354ad | ||
|
|
e3faf676cd | ||
|
|
630e016805 | ||
|
|
23b8b8135c | ||
|
|
7f0b4c079a | ||
|
|
47649cdedc | ||
|
|
7915798ca2 | ||
|
|
86cd88910a | ||
|
|
229ccc75f0 | ||
|
|
6f3e3a7957 | ||
|
|
1f1996b909 | ||
|
|
c2069da919 | ||
|
|
458957ddfb | ||
|
|
95c266fbf3 | ||
|
|
4857940165 | ||
|
|
4c780d09e7 | ||
|
|
d616867cd1 | ||
|
|
909c4ad3bc | ||
|
|
44eff643a9 | ||
|
|
a223e57690 | ||
|
|
69bf2d7081 | ||
|
|
c64644a331 | ||
|
|
e8b82ca687 | ||
|
|
47bd8d1d26 | ||
|
|
a6e88a63d5 | ||
|
|
e6be9a7d13 | ||
|
|
8e34925af7 | ||
|
|
1ec6cefc16 | ||
|
|
4a4c5c3e0f | ||
|
|
19e6a38355 | ||
|
|
066f451a4d | ||
|
|
c50776b991 | ||
|
|
8daa9f71e2 | ||
|
|
340cff5e5b | ||
|
|
1747125ea7 | ||
|
|
6ce650dc15 | ||
|
|
d2b303b47c | ||
|
|
74c5bfa64b | ||
|
|
f826c0e4bb | ||
|
|
b8fc3c6c37 | ||
|
|
22b8c8a62e | ||
|
|
f7ad83d2b5 | ||
|
|
fa8b5d6b71 | ||
|
|
036bff1489 | ||
|
|
fe1854f2d8 | ||
|
|
df9a34dc16 | ||
|
|
e762ea4b90 | ||
|
|
3b725837ac | ||
|
|
23efd8e5d8 | ||
|
|
b61463f570 | ||
|
|
8339233459 | ||
|
|
df68111f0c | ||
|
|
b73ad38156 | ||
|
|
2b7d1ed09f | ||
|
|
f1ef5fa787 | ||
|
|
ec14e07513 | ||
|
|
efdd5fab66 | ||
|
|
4fa114a3f2 | ||
|
|
ab64ff14b1 | ||
|
|
65609c5822 | ||
|
|
bdd41c0434 | ||
|
|
80da793c8d | ||
|
|
ba6c296b14 | ||
|
|
9ae95752ad | ||
|
|
9ba430c53c | ||
|
|
9e605a2717 | ||
|
|
aeab22010f | ||
|
|
2d20f4479c | ||
|
|
6546679def | ||
|
|
4dabd20a2e | ||
|
|
d8e5cb7893 | ||
|
|
28514adf00 | ||
|
|
bfa8aa0a86 | ||
|
|
9e205155a5 | ||
|
|
1e921dd5ea | ||
|
|
5c73e49610 | ||
|
|
91f11e4828 | ||
|
|
4a9c969dc0 | ||
|
|
88e960ae33 | ||
|
|
0217f4f2cc | ||
|
|
29fb8b0b40 | ||
|
|
773ee78fb2 | ||
|
|
d43e28d723 | ||
|
|
d063fdd81d | ||
|
|
f73cd5ec31 | ||
|
|
35df43b727 | ||
|
|
e17999a2d6 | ||
|
|
9f658fc060 | ||
|
|
27ece919d7 | ||
|
|
0e97953adf | ||
|
|
66d44c3a6d | ||
|
|
66ca1b4103 | ||
|
|
0b3040f7b7 | ||
|
|
066f7edfd9 | ||
|
|
72f049452b | ||
|
|
c1b493b7c7 | ||
|
|
67af1ba39e | ||
|
|
c48851719a | ||
|
|
cfbda17cfb | ||
|
|
be32fd4a17 | ||
|
|
98b9e0471c | ||
|
|
9efbbb4f20 | ||
|
|
7b8779fc48 | ||
|
|
07e6042e67 | ||
|
|
814303c521 | ||
|
|
2673b638bf | ||
|
|
92b7b7ae2a | ||
|
|
7d63c2dc11 | ||
|
|
514facacd5 | ||
|
|
89dc8791d0 | ||
|
|
700f65ce73 | ||
|
|
4f24701460 | ||
|
|
0514d51aea | ||
|
|
99a05c66ef | ||
|
|
e533e1472e | ||
|
|
ab00cb11bb | ||
|
|
932b98a634 | ||
|
|
b084b0f79e | ||
|
|
115d479020 | ||
|
|
1cb9a60bba | ||
|
|
14c304ca2d | ||
|
|
88d1ecc7ec | ||
|
|
7cabff0bc4 | ||
|
|
5e0ab39301 | ||
|
|
f6ff4e2d7d | ||
|
|
49c0f3b1c5 | ||
|
|
fbd38aa147 | ||
|
|
922956e981 | ||
|
|
ba1447d5f4 | ||
|
|
9de72a26d0 | ||
|
|
cd8a5bafcf | ||
|
|
8a7b858edb | ||
|
|
ab48622d23 | ||
|
|
6eeba2fdbe | ||
|
|
b26d0d9f9d | ||
|
|
cda29f7b16 | ||
|
|
aaca4b5cb4 | ||
|
|
55460ee730 | ||
|
|
94b0102205 | ||
|
|
dcd1c4235c | ||
|
|
4549e6786f | ||
|
|
2e7685e1ad | ||
|
|
4708557bb3 | ||
|
|
2467b2f984 | ||
|
|
fdd94b95b0 | ||
|
|
dd3cc524ff | ||
|
|
0b71ec18a9 | ||
|
|
2e11d5a4fc | ||
|
|
5cc46fed8f | ||
|
|
191837f1a6 | ||
|
|
890843e394 | ||
|
|
85585db723 | ||
|
|
1719767a47 |
107
README.md
107
README.md
@@ -1,22 +1,30 @@
|
||||
# meshing-around
|
||||
Random Mesh Scripts for Network Testing and BBS Activities for Use with Meshtastic Nodes
|
||||
Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
|
||||
|
||||

|
||||
|
||||
## mesh_bot.sh
|
||||
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example for further processing.
|
||||
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example.
|
||||
|
||||
Along with network testing, this bot has a lot of other features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM.
|
||||
Along with network testing, this bot has a lot of other fun features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM. Or a scheduler to send weather or a reminder weekly for the VHF net.
|
||||
|
||||
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
|
||||
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShportName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
|
||||
|
||||
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages.
|
||||
Look up data using wiki results or interact with [Ollama](https://ollama.com) LLM AI see the [OllamaDocs](https://github.com/ollama/ollama/tree/main/docs) If Ollama is enabled you can DM the bot directly.
|
||||
|
||||
The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
|
||||
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location.
|
||||
|
||||
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
|
||||
|
||||
The bot can also be used to monitor a radio frequency and let you know when high SNR RF activity is seen. Using Hamlib(rigctld) to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
|
||||
|
||||
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
|
||||
|
||||
- Various solar details for radio propagation
|
||||
[Donate$](https://www.paypal.com/donate?token=ZpiU7zDh-AQDyK76nWmWPQLf04iOm-Iyr3f85lpubt37NWGRYtfe11UyC0LmY1wdcC20UubWo4Kec-_G) via PayPal if you like the project!
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
- Various solar details for radio propagation (spaceWeather module)
|
||||
- `sun` and `moon` return info on rise and set local time
|
||||
- `solar` gives an idea of the x-ray flux
|
||||
- `hfcond` returns a table of HF solar conditions
|
||||
@@ -24,14 +32,16 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
|
||||
- `bbshelp` returns the following
|
||||
- `bbslist` list the messages by ID and subject
|
||||
- `bbsread` read a message example use: `bbsread #1`
|
||||
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message`
|
||||
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message or bbspost @nodeShportName #message`
|
||||
- `bbsdelete` delete a message example use: `bbsdelete #4`
|
||||
- Other functions
|
||||
- `whereami` returns the address of location of sender if known
|
||||
- `tide` returns the local tides, NOAA data source
|
||||
- `wx` and `wxc` returns local weather forecast, NOAA data source (wxc is metric value)
|
||||
- `wx` and `wxc` returns local weather forecast, (wxc is metric value), NOAA or Open Meteo for weather forecasting.
|
||||
- `wxa` and `wxalert` return NOAA alerts. Short title or expanded details
|
||||
- `joke` tells a joke
|
||||
- `wiki: ` search wikipedia, return the first few sentances of first result if a match `wiki: lora radio`
|
||||
- `ask: ` ask Ollama LLM AI for a response `ask: what temp do I cook chicken`
|
||||
- `messages` Replay the last messages heard, like Store and Forward
|
||||
- `motd` or to set the message `motd $New Message Of the day`
|
||||
- `lheard` returns the last 5 heard nodes with SNR, can also use `sitrep`
|
||||
@@ -41,19 +51,20 @@ 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.
|
||||
|
||||
### Configurations
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect tcp if possible.
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect tcp if possible. To get BLE mac `meshtastic --ble-scan` **NOTE** I have only tested with a single BLE device and the code is written to only have one interface be a BLE port
|
||||
|
||||
```
|
||||
#config.ini
|
||||
@@ -80,6 +91,14 @@ Setting the default channel is the channel that won't be spammed by the bot. It'
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
```
|
||||
The weather forecasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use.
|
||||
```
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
```
|
||||
|
||||
Modules can be disabled or enabled.
|
||||
```
|
||||
@@ -90,6 +109,19 @@ enabled = False
|
||||
DadJokes = False
|
||||
StoreForward = False
|
||||
```
|
||||
Sentry Bot detects anyone coming close to the bot-node
|
||||
```
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryChannel = 9
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryHoldoff = 2
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
```
|
||||
The BBS has admin and block lists; see the [config.template](config.template)
|
||||
|
||||
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
|
||||
@@ -106,8 +138,8 @@ A module allowing a Hamlib compatible radio to connect to the bot, when function
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
# channel to brodcast to can be 2,3
|
||||
sigWatchBrodcastCh = 2
|
||||
# channel to broadcast to can be 2,3
|
||||
sigWatchBroadcastCh = 2
|
||||
# minimum SNR as reported by radio via hamlib
|
||||
signalDetectionThreshold = -10
|
||||
# hold time for high SNR
|
||||
@@ -116,8 +148,34 @@ signalHoldTime = 10
|
||||
signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
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.4 and likely higher is needed, developed on latest release.
|
||||
|
||||
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
|
||||
|
||||
```
|
||||
pip install meshtastic
|
||||
@@ -132,6 +190,21 @@ pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install geopy
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
The following is needed for open-meteo use
|
||||
```
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
The following is for the Ollama LLM
|
||||
```
|
||||
pip install langchain
|
||||
pip install langchain-ollama
|
||||
pip install ollama
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
|
||||
@@ -139,7 +212,7 @@ To enable emoji in the Debian console, install the fonts `sudo apt-get install f
|
||||
# Recognition
|
||||
I used ideas and snippets from other responder bots and want to call them out!
|
||||
- https://github.com/Murturtle/MeshLink
|
||||
- https://github.com/pdxlocations/Meshtastic-Python-Examples
|
||||
- https://github.com/pdxlocations/meshtastic-Python-Examples
|
||||
- https://github.com/geoffwhittington/meshtastic-matrix-relay
|
||||
|
||||
GitHub user PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
|
||||
|
||||
@@ -32,13 +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
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
StoreLimit = 3
|
||||
# 24 hour clock
|
||||
zuluTime = True
|
||||
zuluTime = False
|
||||
# wait time for URL requests
|
||||
URL_TIMEOUT = 10
|
||||
urlTimeout = 10
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = False
|
||||
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryChannel = 9
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
|
||||
[bbs]
|
||||
enabled = True
|
||||
@@ -52,14 +74,14 @@ bbs_admin_list =
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
# weather forecast days, the first two rows are today and tonight
|
||||
DAYS_OF_WEATHER = 4
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
ALERT_COUNT = 2
|
||||
|
||||
# solar module
|
||||
[solar]
|
||||
enabled = True
|
||||
NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -74,8 +96,8 @@ repeater_channels =
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
# brodcast to all nodes on the channel can alsp be = 2,3
|
||||
sigWatchBrodcastCh = 2
|
||||
# broadcast to all nodes on the channel can alsp be = 2,3
|
||||
sigWatchBroadcastCh = 2
|
||||
# minimum SNR as reported by radio via hamlib
|
||||
signalDetectionThreshold = -10
|
||||
# hold time for high SNR
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,7 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/python /dir/launch.sh mesh
|
||||
ExecStart=/usr/bin/bash /dir/launch.sh mesh
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
@@ -8,7 +8,7 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/python /dir/launch.sh pong
|
||||
ExecStart=/usr/bin/bash /dir/launch.sh pong
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
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
|
||||
|
||||
@@ -14,9 +14,9 @@ fi
|
||||
|
||||
# launch the application
|
||||
if [ "$1" == "pong" ]; then
|
||||
python pong_bot.py
|
||||
python3 pong_bot.py
|
||||
elif [ "$1" == "mesh" ]; then
|
||||
python mesh_bot.py
|
||||
python3 mesh_bot.py
|
||||
else
|
||||
printf "\nPlease provide a bot to launch (pong/mesh)"
|
||||
fi
|
||||
|
||||
575
mesh_bot.py
575
mesh_bot.py
@@ -5,159 +5,282 @@
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from pubsub import pub # pip install pubsub
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
|
||||
#Auto response to messages
|
||||
if "ping" in message.lower():
|
||||
#Check if the user added @foo to the message
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
bot_response = "🏓PONG, " + hop
|
||||
elif "ack" in message.lower():
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
bot_response = "🏓ACK-ACK! " + hop
|
||||
elif "pong" in message.lower():
|
||||
bot_response = "🏓PING!!"
|
||||
elif "motd" in message.lower():
|
||||
#check if the user wants to set the motd by using $
|
||||
if "$" in message:
|
||||
motd = message.split("$")[1]
|
||||
global MOTD
|
||||
MOTD = motd
|
||||
bot_response = "MOTD Set to: " + MOTD
|
||||
else:
|
||||
bot_response = MOTD
|
||||
elif "messages" in message.lower():
|
||||
response = ""
|
||||
for msgH in msg_history:
|
||||
# check if the message is from the same interface
|
||||
if msgH[4] == deviceID:
|
||||
# check if the message is from the same channel
|
||||
if msgH[2] == channel_number or msgH[2] == publicChannel:
|
||||
# consider message safe to send
|
||||
response += f"\n{msgH[0]}: {msgH[1]}"
|
||||
|
||||
if len(response) > 0:
|
||||
bot_response = "Message History:" + response
|
||||
else:
|
||||
bot_response = "No messages in history"
|
||||
elif "bbshelp" in message.lower():
|
||||
bot_response = bbs_help()
|
||||
elif "cmd" in message.lower() or "cmd?" in message.lower():
|
||||
bot_response = help_message
|
||||
elif "sun" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
bot_response = get_sun(str(location[0]),str(location[1]))
|
||||
elif "hfcond" in message.lower():
|
||||
bot_response = hf_band_conditions()
|
||||
elif "solar" in message.lower():
|
||||
bot_response = drap_xray_conditions() + "\n" + solar_conditions()
|
||||
elif "lheard" in message.lower() or "sitrep" in message.lower():
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
bot_response += "Ch Use: " + str(chutil1) + "%"
|
||||
if interface2_enabled:
|
||||
bot_response += " P2:" + str(chutil2) + "%"
|
||||
elif "whereami" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
where = where_am_i(str(location[0]),str(location[1]))
|
||||
bot_response = where
|
||||
elif "tide" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
tide = get_tide(str(location[0]),str(location[1]))
|
||||
bot_response = tide
|
||||
elif "moon" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
moon = get_moon(str(location[0]),str(location[1]))
|
||||
bot_response = moon
|
||||
elif "wxalert" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
|
||||
bot_response = weatherAlert
|
||||
elif "wxa" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
weatherAlert = getWeatherAlerts(str(location[0]),str(location[1]))
|
||||
bot_response = weatherAlert
|
||||
elif "wxc" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
weather = get_weather(str(location[0]),str(location[1]),1)
|
||||
bot_response = weather
|
||||
elif "wx" in message.lower():
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
weather = get_weather(str(location[0]),str(location[1]))
|
||||
bot_response = weather
|
||||
elif "joke" in message.lower():
|
||||
bot_response = tell_joke()
|
||||
elif "bbslist" in message.lower():
|
||||
bot_response = bbs_list_messages()
|
||||
elif "bbspost" in message.lower():
|
||||
# Check if the user added a subject to the message
|
||||
if "$" in message:
|
||||
subject = message.split("$")[1].split("#")[0]
|
||||
subject = subject.rstrip()
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
body = body.rstrip()
|
||||
print(f"{log_timestamp()} System: BBS Post: {subject} Body: {body}")
|
||||
bot_response = bbs_post_message(subject,body,message_from_id)
|
||||
else:
|
||||
bot_response = "example: bbspost $subject #message"
|
||||
# Check if the user added a node number to the message
|
||||
elif "@" in message:
|
||||
toNode = message.split("@")[1].split("#")[0]
|
||||
toNode = toNode.rstrip()
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
bot_response = bbs_post_dm(toNode, body, message_from_id)
|
||||
else:
|
||||
bot_response = "example: bbspost @nodeNumber #message"
|
||||
else:
|
||||
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
|
||||
message_lower = message.lower()
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
"ping": lambda: handle_ping(message, hop, snr, rssi),
|
||||
"pong": lambda: "🏓PING!!",
|
||||
"motd": lambda: handle_motd(message),
|
||||
"bbshelp": bbs_help,
|
||||
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
|
||||
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
|
||||
"wiki:": lambda: handle_wiki(message),
|
||||
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
"joke": tell_joke,
|
||||
"bbslist": bbs_list_messages,
|
||||
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
|
||||
"messages": lambda: handle_messages(deviceID, channel_number, msg_history, publicChannel),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"hfcond": hf_band_conditions,
|
||||
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
|
||||
"lheard": lambda: handle_lheard(),
|
||||
"sitrep": lambda: handle_lheard(),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
|
||||
"ack": lambda: handle_ack(hop, snr, rssi),
|
||||
"testing": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_testing(hop, snr, rssi),
|
||||
}
|
||||
cmds = [] # list to hold the commands found in the message
|
||||
for key in command_handler:
|
||||
if key in message_lower.split(' '):
|
||||
cmds.append({'cmd': key, 'index': message_lower.index(key)})
|
||||
|
||||
if len(cmds) > 0:
|
||||
# sort the commands by index value
|
||||
cmds = sorted(cmds, key=lambda k: k['index'])
|
||||
logger.debug(f"System: Bot detected Commands:{cmds}")
|
||||
# run the first command after sorting
|
||||
bot_response = command_handler[cmds[0]['cmd']]()
|
||||
|
||||
elif "bbsread" in message.lower():
|
||||
# Check if the user added a message number to the message
|
||||
if "#" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
bot_response = bbs_read_message(messageID)
|
||||
else:
|
||||
bot_response = "Please add a message number ex: bbsread #14"
|
||||
elif "bbsdelete" in message.lower():
|
||||
# Check if the user added a message number to the message
|
||||
if "#" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
bot_response = bbs_delete_message(messageID, message_from_id)
|
||||
else:
|
||||
bot_response = "Please add a message number ex: bbsdelete #14"
|
||||
elif "testing" in message.lower() or "test" in message.lower():
|
||||
bot_response = "🏓Testing 1,2,3"
|
||||
else:
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
# wait a 700ms to avoid message collision from lora-ack
|
||||
time.sleep(0.7)
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_ping(message, hop, snr, rssi):
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓PONG, " + hop
|
||||
|
||||
def handle_motd(message):
|
||||
global MOTD
|
||||
if "$" in message:
|
||||
motd = message.split("$")[1]
|
||||
MOTD = motd.rstrip()
|
||||
return "MOTD Set to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def handle_wxalert(message_from_id, deviceID, message):
|
||||
if use_meteo_wxApi:
|
||||
return "wxalert is not supported"
|
||||
else:
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if "wxalert" in message:
|
||||
# Detailed weather alert
|
||||
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
|
||||
else:
|
||||
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
|
||||
|
||||
return weatherAlert
|
||||
|
||||
def handle_wiki(message):
|
||||
if "wiki:" in message.lower():
|
||||
search = message.split(":")[1]
|
||||
search = search.strip()
|
||||
return get_wikipedia_summary(search)
|
||||
else:
|
||||
return "Please add a search term example:wiki: travelling gnome"
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmTotalRuntime
|
||||
if "ask:" in message.lower():
|
||||
user_input = message.split(":")[1]
|
||||
user_input = user_input.strip()
|
||||
else:
|
||||
user_input = message
|
||||
|
||||
if len(user_input) < 1:
|
||||
return "Please ask a question"
|
||||
|
||||
# information for the user on how long the query will take on average
|
||||
if llmRunCounter > 0:
|
||||
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
|
||||
if averageRuntime > 25:
|
||||
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
|
||||
if channel_number == publicChannel:
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
else:
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
else:
|
||||
msg = "Please wait, response could take 3+ minutes. Fund the SysOp's GPU budget!"
|
||||
if channel_number == publicChannel:
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
else:
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
|
||||
start = time.time()
|
||||
|
||||
#response = asyncio.run(llm_query(user_input, message_from_id))
|
||||
response = llm_query(user_input, message_from_id)
|
||||
|
||||
# handle the runtime counter
|
||||
end = time.time()
|
||||
llmRunCounter += 1
|
||||
llmTotalRuntime.append(end - start)
|
||||
|
||||
return response
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
|
||||
logger.debug(f"System: Bot Returning Open-Meteo API for weather imperial")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]))
|
||||
elif use_meteo_wxApi:
|
||||
logger.debug(f"System: Bot Returning Open-Meteo API for weather metric")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
|
||||
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
|
||||
logger.debug(f"System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_weather(str(location[0]), str(location[1]), 1)
|
||||
else:
|
||||
logger.debug(f"System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_weather(str(location[0]), str(location[1]))
|
||||
return weather
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
if "$" in message and not "example:" in message:
|
||||
subject = message.split("$")[1].split("#")[0]
|
||||
subject = subject.rstrip()
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
body = body.rstrip()
|
||||
logger.info(f"System: BBS Post: {subject} Body: {body}")
|
||||
return bbs_post_message(subject, body, message_from_id)
|
||||
elif not "example:" in message:
|
||||
return "example: bbspost $subject #message"
|
||||
elif "@" in message and not "example:" in message:
|
||||
toNode = message.split("@")[1].split("#")[0]
|
||||
toNode = toNode.rstrip()
|
||||
if toNode.isalpha() or not toNode.isnumeric():
|
||||
toNode = get_num_from_short_name(toNode, deviceID)
|
||||
if toNode == 0:
|
||||
return "Node not found " + message.split("@")[1].split("#")[0]
|
||||
if "#" in message:
|
||||
body = message.split("#")[1]
|
||||
return bbs_post_dm(toNode, body, message_from_id)
|
||||
else:
|
||||
return "example: bbspost @nodeNumber/ShortName #message"
|
||||
elif not "example:" in message:
|
||||
return "example: bbspost $subject #message, or bbspost @node #message"
|
||||
|
||||
def handle_bbsread(message):
|
||||
if "#" in message and not "example:" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
return bbs_read_message(messageID)
|
||||
elif not "example:" in message:
|
||||
return "Please add a message number example: bbsread #14"
|
||||
|
||||
def handle_bbsdelete(message, message_from_id):
|
||||
if "#" in message and not "example:" in message:
|
||||
messageID = int(message.split("#")[1])
|
||||
return bbs_delete_message(messageID, message_from_id)
|
||||
elif not "example:" in message:
|
||||
return "Please add a message number example: bbsdelete #14"
|
||||
|
||||
def handle_messages(deviceID, channel_number, msg_history, publicChannel):
|
||||
response = ""
|
||||
for msgH in msg_history:
|
||||
if msgH[4] == deviceID:
|
||||
if msgH[2] == channel_number or msgH[2] == publicChannel:
|
||||
response += f"\n{msgH[0]}: {msgH[1]}"
|
||||
if len(response) > 0:
|
||||
return "Message History:" + response
|
||||
else:
|
||||
return "No messages in history"
|
||||
|
||||
def handle_sun(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_lheard():
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
bot_response += "Ch Use: " + str(chutil1) + "%"
|
||||
if interface2_enabled:
|
||||
bot_response += " P2:" + str(chutil2) + "%"
|
||||
return bot_response
|
||||
|
||||
def handle_whereami(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return where_am_i(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_tide(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_tide(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_moon(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_moon(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
rxType = type(interface).__name__
|
||||
rxNode = 0
|
||||
#logger.debug(f"System: Packet Received on {rxType}")
|
||||
# Debug print the interface object
|
||||
#for item in interface.__dict__.items(): print (item)
|
||||
|
||||
@@ -175,6 +298,12 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# Debug print the packet for debugging
|
||||
#print(f"Packet Received\n {packet} \n END of packet \n")
|
||||
message_from_id = 0
|
||||
@@ -192,7 +321,7 @@ def onReceive(packet, interface):
|
||||
if msg:
|
||||
# wait a 700ms to avoid message collision from lora-ack.
|
||||
time.sleep(0.7)
|
||||
print(f"{log_timestamp()} System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.info(f"System: BBS DM Found: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
|
||||
bbs_delete_dm(msg[0], msg[1])
|
||||
send_message(message, channel_number, message_from_id, rxNode)
|
||||
@@ -203,31 +332,29 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
try:
|
||||
snr = packet['rxSnr']
|
||||
rssi = packet['rxRssi']
|
||||
except KeyError:
|
||||
snr = 0
|
||||
rssi = 0
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
snr = packet.get('rxSnr', 0)
|
||||
rssi = packet.get('rxRssi', 0)
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet['channel']
|
||||
else:
|
||||
channel_number = publicChannel
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet['hopsAway']
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
hop_away = 0
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet['hopLimit']
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet['hopStart']
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
@@ -245,26 +372,35 @@ def onReceive(packet, interface):
|
||||
|
||||
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
print(f"{log_timestamp()} Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
return
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
|
||||
# message is DM to us
|
||||
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if messageTrap(message_string):
|
||||
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
if llm_enabled:
|
||||
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
|
||||
# log the message to the message log
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
@@ -272,7 +408,7 @@ def onReceive(packet, interface):
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
@@ -280,8 +416,8 @@ def onReceive(packet, interface):
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
|
||||
else:
|
||||
# ignore the message but add it to the message history and repeat it if enabled
|
||||
# add the message to the message history but limit
|
||||
# message is not for bot to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
@@ -292,57 +428,102 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
msg_history.pop(0)
|
||||
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
|
||||
|
||||
# check if repeater is enabled and the other interface is enabled
|
||||
|
||||
# print the message to the log and sdout
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
|
||||
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
# repeat the message on the other device
|
||||
# wait a 700ms to avoid message collision from lora-ack.
|
||||
time.sleep(0.7)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
if rxNode == 1:
|
||||
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
|
||||
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 2)
|
||||
elif rxNode == 2:
|
||||
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
|
||||
except KeyError as e:
|
||||
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
print(packet) # print the packet for debugging
|
||||
print("END of packet \n")
|
||||
|
||||
async def start_rx():
|
||||
print ("\nMeshtastic Autoresponder Bot CTL+C to exit\n")
|
||||
if bbs_enabled:
|
||||
print(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
|
||||
if solar_conditions_enabled:
|
||||
print(f"System: Celestial Telemetry Enabled")
|
||||
if location_enabled:
|
||||
print(f"System: Location Telemetry Enabled")
|
||||
if dad_jokes_enabled:
|
||||
print(f"System: Dad Jokes Enabled!")
|
||||
if store_forward_enabled:
|
||||
print(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
print(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
print(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_dectection_enabled:
|
||||
print(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model please wait")
|
||||
llm_query(" ", myNodeNum1)
|
||||
logger.debug(f"System: LLM model loaded")
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
|
||||
msg = (f"{log_timestamp()} System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
print (msg)
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
msg = (f"{log_timestamp()} System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
print (msg)
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
if log_messages_to_file:
|
||||
logger.debug(f"System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
logger.debug(f"System: Logging System Logs to disk")
|
||||
if bbs_enabled:
|
||||
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
|
||||
if solar_conditions_enabled:
|
||||
logger.debug(f"System: Celestial Telemetry Enabled")
|
||||
if location_enabled:
|
||||
if use_meteo_wxApi:
|
||||
logger.debug(f"System: Location Telemetry Enabled using Open-Meteo API")
|
||||
else:
|
||||
logger.debug(f"System: Location Telemetry Enabled using NOAA API")
|
||||
if dad_jokes_enabled:
|
||||
logger.debug(f"System: Dad Jokes Enabled!")
|
||||
if wikipedia_enabled:
|
||||
logger.debug(f"System: Wikipedia search Enabled")
|
||||
if motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
|
||||
|
||||
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
|
||||
|
||||
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
|
||||
|
||||
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
|
||||
|
||||
#
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
@@ -353,7 +534,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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import pickle # pip install pickle
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
|
||||
|
||||
@@ -18,14 +18,14 @@ def load_bbsdb():
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
print ("\nSystem: Creating new bbsdb.pkl")
|
||||
logger.debug("System: Creating new bbsdb.pkl")
|
||||
with open('bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
def save_bbsdb():
|
||||
global bbs_messages
|
||||
# save the bbs messages to the database file
|
||||
print ("System: Saving bbsdb.pkl\n")
|
||||
logger.debug("System: Saving bbsdb.pkl")
|
||||
with open('bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
@@ -64,7 +64,7 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
|
||||
|
||||
return "Msg #" + str(messageID) + " deleted."
|
||||
else:
|
||||
print (f"!!System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
|
||||
logger.warning(f"System: node {fromNode}, tried to delete a message: {bbs_messages[messageID - 1]} and was dropped.")
|
||||
return "You are not authorized to delete this message."
|
||||
else:
|
||||
return "Please specify a message number to delete."
|
||||
@@ -75,12 +75,12 @@ def bbs_post_message(subject, message, fromNode):
|
||||
|
||||
# Check the BAN list for naughty nodes and silently drop the message
|
||||
if str(fromNode) in bbs_ban_list:
|
||||
print (f"!!System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
|
||||
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
|
||||
return "Message posted. ID is: " + str(messageID)
|
||||
|
||||
# append the message to the list
|
||||
bbs_messages.append([messageID, subject, message, fromNode])
|
||||
print (f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
|
||||
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
|
||||
|
||||
# save the bbsdb
|
||||
save_bbsdb()
|
||||
@@ -100,7 +100,7 @@ def bbs_read_message(messageID = 0):
|
||||
def save_bbsdm():
|
||||
global bbs_dm
|
||||
# save the bbs messages to the database file
|
||||
print ("System: Saving Updated BBS Direct Messages bbsdm.pkl")
|
||||
logger.debug("System: Saving Updated BBS Direct Messages bbsdm.pkl")
|
||||
with open('bbsdm.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_dm, f)
|
||||
|
||||
@@ -112,7 +112,7 @@ def load_bbsdm():
|
||||
bbs_dm = pickle.load(f)
|
||||
except:
|
||||
bbs_dm = [[1234567890, "Message", 1234567890]]
|
||||
print ("\nSystem: Creating new bbsdm.pkl")
|
||||
logger.debug("System: Creating new bbsdm.pkl")
|
||||
with open('bbsdm.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_dm, f)
|
||||
|
||||
@@ -120,7 +120,7 @@ def bbs_post_dm(toNode, message, fromNode):
|
||||
global bbs_dm
|
||||
# Check the BAN list for naughty nodes and silently drop the message
|
||||
if str(fromNode) in bbs_ban_list:
|
||||
print (f"!!System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
|
||||
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {message} and was dropped.")
|
||||
return "DM Posted for node " + str(toNode)
|
||||
|
||||
# append the message to the list
|
||||
|
||||
51
modules/llm.py
Normal file
51
modules/llm.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
# LLM Module vDev
|
||||
from modules.log import *
|
||||
|
||||
from langchain_ollama import OllamaLLM
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
meshBotAI = """
|
||||
FROM llama3.1
|
||||
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'.
|
||||
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
|
||||
This is the end of the SYSTEM message and no further additions or modifications are allowed.
|
||||
|
||||
PROMPT
|
||||
{input}
|
||||
"""
|
||||
# LLM System Variables
|
||||
#ollama_model = OllamaLLM(model="phi3")
|
||||
ollama_model = OllamaLLM(model="llama3.1")
|
||||
model_prompt = ChatPromptTemplate.from_template(meshBotAI)
|
||||
chain_prompt_model = model_prompt | ollama_model
|
||||
antiFloodLLM = []
|
||||
|
||||
trap_list_llm = ("ask:",)
|
||||
|
||||
def llm_query(input, nodeID=0):
|
||||
global antiFloodLLM
|
||||
|
||||
# add the naughty list here to stop the function before we continue
|
||||
# add a list of allowed nodes only to use the function
|
||||
|
||||
# anti flood protection
|
||||
if nodeID in antiFloodLLM:
|
||||
return "Please wait before sending another message"
|
||||
else:
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
response = ""
|
||||
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
|
||||
|
||||
result = chain_prompt_model.invoke({"input": input})
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
# done with the query, remove the user from the anti flood list
|
||||
antiFloodLLM.remove(nodeID)
|
||||
|
||||
return response
|
||||
@@ -1,4 +1,4 @@
|
||||
# helper functions to use location data
|
||||
# helper functions to use location data like NOAA weather
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
@@ -7,7 +7,7 @@ import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
|
||||
|
||||
@@ -15,7 +15,8 @@ def where_am_i(lat=0, lon=0):
|
||||
whereIam = ""
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
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
|
||||
@@ -41,6 +42,7 @@ def where_am_i(lat=0, lon=0):
|
||||
def get_tide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
return NO_DATA_NOGPS
|
||||
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
|
||||
try:
|
||||
@@ -48,14 +50,17 @@ def get_tide(lat=0, lon=0):
|
||||
if station_data.ok:
|
||||
station_json = station_data.json()
|
||||
else:
|
||||
logger.error("Location:Error fetching tide station table from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
if station_json['stationList'] == [] or station_json['stationList'] is None:
|
||||
logger.error("Location:No tide station found")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
station_id = station_json['stationList'][0]['stationId']
|
||||
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError):
|
||||
logger.error("Location:Error fetching tide station table from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
|
||||
@@ -65,8 +70,10 @@ def get_tide(lat=0, lon=0):
|
||||
try:
|
||||
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
|
||||
if not station_data.ok:
|
||||
logger.error("Location:Error fetching station data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching station data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# extract table class="table table-condensed"
|
||||
@@ -97,7 +104,10 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
return NO_DATA_NOGPS
|
||||
|
||||
# get weather data from NOAA units for metric
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
unit = 1
|
||||
|
||||
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
|
||||
if unit == 1:
|
||||
weather_url += "&unit=1"
|
||||
@@ -105,14 +115,17 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
try:
|
||||
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
|
||||
if not weather_data.ok:
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
|
||||
table = soup.find('div', id="detailed-forecast-body")
|
||||
|
||||
if table is None:
|
||||
logger.error("Location:Bad weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
# get rows
|
||||
@@ -179,7 +192,9 @@ def abbreviate_weather(row):
|
||||
"West": "W",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms"
|
||||
"thunderstorms": "t-storms",
|
||||
"quarters": "qtrs",
|
||||
"quarter": "qtr"
|
||||
}
|
||||
|
||||
line = row
|
||||
@@ -196,12 +211,15 @@ def getWeatherAlerts(lat=0, lon=0):
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
#logger.debug("Location:Fetching weather alerts from NOAA for " + str(lat) + ", " + str(lon))
|
||||
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
alerts = ""
|
||||
@@ -233,16 +251,20 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.warning("Location:No GPS data, try sending location for weather alerts")
|
||||
return NO_DATA_NOGPS
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
#logger.debug("Location:Fetching weather alerts detailed from NOAA for " + str(lat) + ", " + str(lon))
|
||||
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching weather alerts from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
alerts = ""
|
||||
|
||||
72
modules/log.py
Normal file
72
modules/log.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Custom logger for MeshBot and PongBot
|
||||
# you can change the sdtout_handler level to logging.INFO to only show INFO level logs
|
||||
# stdout_handler.setLevel(logging.INFO)vs stdout_handler.setLevel(logging.DEBUG)
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from modules.settings import *
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
grey = '\x1b[38;21m'
|
||||
white = '\x1b[38;5;231m'
|
||||
blue = '\x1b[38;5;39m'
|
||||
yellow = '\x1b[38;5;226m'
|
||||
red = '\x1b[38;5;196m'
|
||||
green = '\x1b[38;5;46m'
|
||||
purple = '\x1b[38;5;129m'
|
||||
bold_red = '\x1b[31;1m'
|
||||
bold_white = '\x1b[37;1m'
|
||||
reset = '\x1b[0m'
|
||||
|
||||
def __init__(self, fmt):
|
||||
super().__init__()
|
||||
self.fmt = fmt
|
||||
self.FORMATS = {
|
||||
logging.DEBUG: self.blue + self.fmt + self.reset,
|
||||
logging.INFO: self.white + self.fmt + self.reset,
|
||||
logging.WARNING: self.yellow + self.fmt + self.reset,
|
||||
logging.ERROR: self.red + self.fmt + self.reset,
|
||||
logging.CRITICAL: self.bold_red + self.fmt + self.reset
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.propagate = False
|
||||
|
||||
msgLogger = logging.getLogger("MeshBot Messages Logger")
|
||||
msgLogger.setLevel(logging.INFO)
|
||||
msgLogger.propagate = False
|
||||
|
||||
# Define format for logs
|
||||
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
|
||||
msgLogFormat = '%(asctime)s | %(message)s'
|
||||
today = datetime.now()
|
||||
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
# Set level for stdout handler (logs DEBUG level and above)
|
||||
stdout_handler.setLevel(logging.DEBUG)
|
||||
# Set format for stdout handler
|
||||
stdout_handler.setFormatter(CustomFormatter(logFormat))
|
||||
|
||||
# Add handlers to the logger
|
||||
logger.addHandler(stdout_handler)
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('system{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler.setFormatter(logging.Formatter(logFormat))
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import socket
|
||||
import asyncio
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
|
||||
def get_hamlib(msg="f"):
|
||||
try:
|
||||
@@ -13,7 +13,7 @@ def get_hamlib(msg="f"):
|
||||
rigControlSocket.settimeout(2)
|
||||
rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1])))
|
||||
except Exception as e:
|
||||
print(f"\nSystem: Error connecting to rigctld: {e}")
|
||||
logger.error(f"RadioMon: Error connecting to rigctld: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
try:
|
||||
@@ -27,7 +27,7 @@ def get_hamlib(msg="f"):
|
||||
data = data.replace(b'\n',b'')
|
||||
return data.decode("utf-8").rstrip()
|
||||
except Exception as e:
|
||||
print(f"\nSystem: Error fetching data from rigctld: {e}")
|
||||
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def get_freq_common_name(freq):
|
||||
@@ -140,6 +140,7 @@ async def signalWatcher():
|
||||
signalStrength = int(get_sig_strength())
|
||||
if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold:
|
||||
message = f"Detected {get_freq_common_name(get_hamlib('f'))} active. S-Meter:{signalStrength}dBm"
|
||||
logger.debug(f"RadioMon: {message}. Waiting for {signalHoldTime} seconds")
|
||||
previousStrength = signalStrength
|
||||
signalCycle = 0
|
||||
await asyncio.sleep(signalHoldTime)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Settings for MeshBot and PongBot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
import configparser
|
||||
|
||||
# messages
|
||||
@@ -22,6 +24,10 @@ max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
scheduler_enabled = False # enable the scheduler currently config via code only
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -40,6 +46,26 @@ if config.sections() == []:
|
||||
config.write(open(config_file, 'w'))
|
||||
print (f"System: Config file created, check {config_file} or review the config.template")
|
||||
|
||||
if 'sentry' not in config:
|
||||
config['Sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'location' not in config:
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'bbs' not in config:
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'repeater' not in config:
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'radioMon' not in config:
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
@@ -58,38 +84,55 @@ 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)
|
||||
latitudeValue = config['location'].getfloat('lat', 48.50)
|
||||
longitudeValue = config['location'].getfloat('lon', -123.0)
|
||||
zuluTime = config['general'].getboolean('zuluTime', False)
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', False)
|
||||
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True)
|
||||
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
|
||||
welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG)
|
||||
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
|
||||
solar_conditions_enabled = config['solar'].getboolean('enabled', False)
|
||||
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
|
||||
|
||||
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
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', False)
|
||||
config['general'].get('motd', MOTD)
|
||||
urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds
|
||||
forecastDuration = config['general'].getint('DAYS_OF_WEATHER', 4) # default days of weather
|
||||
numWxAlerts = config['general'].getint('ALERT_COUNT', 2) # default 2 alerts
|
||||
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
||||
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
|
||||
|
||||
repeater_enabled = config['repeater'].getboolean('enabled', False)
|
||||
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
|
||||
radio_dectection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
|
||||
sigWatchBrodcastCh = config['radioMon'].get('sigWatchBrodcastCh', '2').split(',') # default Channel 2
|
||||
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
|
||||
signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm
|
||||
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
|
||||
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
|
||||
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
print(f"System: Check the config.ini against config.template file for missing sections or values.")
|
||||
print(f"System: Exiting...")
|
||||
exit(1)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# helper functions to get HF band conditions, DRAP X-ray flux, and sunrise/sunset times
|
||||
# some code from https://github.com/Murturtle/MeshLink
|
||||
# HF code from https://github.com/Murturtle/MeshLink
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import requests # pip install requests
|
||||
@@ -7,7 +7,7 @@ import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond")
|
||||
|
||||
@@ -19,9 +19,11 @@ def hf_band_conditions():
|
||||
solarxml = xml.dom.minidom.parseString(band_cond.text)
|
||||
for i in solarxml.getElementsByTagName("band"):
|
||||
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
|
||||
hf_cond = hf_cond[:-1] # remove the last newline
|
||||
else:
|
||||
hf_cond += ERROR_FETCHING_DATA
|
||||
hf_cond = hf_cond[:-1] # remove the last newline
|
||||
logger.error("Solar: Error fetching HF band conditions")
|
||||
hf_cond = ERROR_FETCHING_DATA
|
||||
|
||||
return hf_cond
|
||||
|
||||
def solar_conditions():
|
||||
@@ -39,7 +41,8 @@ def solar_conditions():
|
||||
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
|
||||
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
|
||||
else:
|
||||
solar_cond += ERROR_FETCHING_DATA
|
||||
logger.error("Solar: Error fetching solar conditions")
|
||||
solar_cond = ERROR_FETCHING_DATA
|
||||
return solar_cond
|
||||
|
||||
def drap_xray_conditions():
|
||||
@@ -53,7 +56,8 @@ def drap_xray_conditions():
|
||||
if x_filter in line:
|
||||
xray_flux = line.split(": ")[1]
|
||||
else:
|
||||
xray_flux += ERROR_FETCHING_DATA
|
||||
logger.error("Error fetching DRAP X-ray flux")
|
||||
xray_flux = ERROR_FETCHING_DATA
|
||||
return xray_flux
|
||||
|
||||
def get_sun(lat=0, lon=0):
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# helper functions for system related tasks
|
||||
# helper functions and init for system related tasks
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import meshtastic.serial_interface #pip install meshtastic
|
||||
import meshtastic.tcp_interface
|
||||
import meshtastic.ble_interface
|
||||
from datetime import datetime
|
||||
import time
|
||||
import asyncio
|
||||
from modules.settings import *
|
||||
import contextlib # for suppressing output on watchdog
|
||||
from modules.log import *
|
||||
|
||||
# Global Variables
|
||||
trap_list = ("cmd","cmd?") # default trap list
|
||||
@@ -21,22 +21,36 @@ if ping_enabled:
|
||||
trap_list = trap_list + trap_list_ping
|
||||
help_message = help_message + "ping"
|
||||
|
||||
# Sitrep Configuration
|
||||
if sitrep_enabled:
|
||||
trap_list_sitrep = ("sitrep", "lheard")
|
||||
trap_list = trap_list + trap_list_sitrep
|
||||
help_message = help_message + ", sitrep"
|
||||
|
||||
# MOTD Configuration
|
||||
if motd_enabled:
|
||||
trap_list_motd = ("motd",)
|
||||
trap_list = trap_list + trap_list_motd
|
||||
help_message = help_message + ", motd"
|
||||
|
||||
# Solar Conditions Configuration
|
||||
if solar_conditions_enabled:
|
||||
from modules.solarconditions import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
|
||||
help_message = help_message + ", sun, hfcond, solar, moon, tide"
|
||||
help_message = help_message + ", sun, hfcond, solar, moon"
|
||||
|
||||
# Location Configuration
|
||||
if location_enabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
|
||||
help_message = help_message + ", whereami, wx, wxc, wxa"
|
||||
help_message = help_message + ", whereami, wx, wxc"
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
|
||||
else:
|
||||
# NOAA only features
|
||||
help_message = help_message + ", wxa, tide"
|
||||
|
||||
# BBS Configuration
|
||||
if bbs_enabled:
|
||||
@@ -50,17 +64,46 @@ if dad_jokes_enabled:
|
||||
trap_list = trap_list + ("joke",)
|
||||
help_message = help_message + ", joke"
|
||||
|
||||
# Wikipedia Search Configuration
|
||||
if wikipedia_enabled:
|
||||
import wikipedia # pip install wikipedia
|
||||
trap_list = trap_list + ("wiki:",)
|
||||
help_message = help_message + ", wiki:"
|
||||
|
||||
# LLM Configuration
|
||||
if llm_enabled:
|
||||
from modules.llm import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_llm # items ask:
|
||||
help_message = help_message + ", ask:"
|
||||
|
||||
# Scheduled Broadcast Configuration
|
||||
if scheduler_enabled:
|
||||
import schedule # pip install schedule
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
|
||||
# Sentry Configuration
|
||||
if sentry_enabled:
|
||||
from math import sqrt
|
||||
import geopy.distance # pip install geopy
|
||||
|
||||
# Store and Forward Configuration
|
||||
if store_forward_enabled:
|
||||
trap_list = trap_list + ("messages",)
|
||||
help_message = help_message + ", messages"
|
||||
|
||||
# Radio Monitor Configuration
|
||||
if radio_dectection_enabled:
|
||||
if radio_detection_enabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# BLE dual interface prevention
|
||||
if interface1_type == 'ble' and interface2_type == 'ble':
|
||||
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
|
||||
exit()
|
||||
|
||||
# Interface1 Configuration
|
||||
try:
|
||||
logger.debug(f"System: Initalizing Interface1")
|
||||
if interface1_type == 'serial':
|
||||
interface1 = meshtastic.serial_interface.SerialInterface(port1)
|
||||
elif interface1_type == 'tcp':
|
||||
@@ -68,14 +111,15 @@ try:
|
||||
elif interface1_type == 'ble':
|
||||
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
|
||||
else:
|
||||
print(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
|
||||
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
print(f"System: Critical Error script abort. Initalizing Interface1 {e}")
|
||||
logger.critical(f"System: script abort. Initalizing Interface1 {e}")
|
||||
exit()
|
||||
|
||||
# Interface2 Configuration
|
||||
if interface2_enabled:
|
||||
logger.debug(f"System: Initalizing Interface2")
|
||||
try:
|
||||
if interface2_type == 'serial':
|
||||
interface2 = meshtastic.serial_interface.SerialInterface(port2)
|
||||
@@ -84,10 +128,10 @@ if interface2_enabled:
|
||||
elif interface2_type == 'ble':
|
||||
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
|
||||
else:
|
||||
print(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
|
||||
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
print(f"System: Critical Error script abort. Initalizing Interface2 {e}")
|
||||
logger.critical(f"System: script abort. Initalizing Interface2 {e}")
|
||||
exit()
|
||||
|
||||
#Get the node number of the device, check if the device is connected
|
||||
@@ -95,7 +139,7 @@ try:
|
||||
myinfo = interface1.getMyNodeInfo()
|
||||
myNodeNum1 = myinfo['num']
|
||||
except Exception as e:
|
||||
print(f"System: Critical Error script abort. {e}")
|
||||
logger.critical(f"System: script abort. {e}")
|
||||
exit()
|
||||
|
||||
if interface2_enabled:
|
||||
@@ -103,16 +147,12 @@ if interface2_enabled:
|
||||
myinfo2 = interface2.getMyNodeInfo()
|
||||
myNodeNum2 = myinfo2['num']
|
||||
except Exception as e:
|
||||
print(f"System: Critical Error script abort. {e}")
|
||||
logger.critical(f"System: script abort. {e}")
|
||||
exit()
|
||||
else:
|
||||
myNodeNum2 = 777
|
||||
|
||||
def log_timestamp():
|
||||
if zuluTime:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
return datetime.now().strftime("%Y-%m-%d %I:%M:%S%p")
|
||||
# functions below
|
||||
|
||||
def decimal_to_hex(decimal_number):
|
||||
return f"!{decimal_number:08x}"
|
||||
@@ -149,6 +189,40 @@ def get_name_from_number(number, type='long', nodeInt=1):
|
||||
name = str(decimal_to_hex(number)) # If name not found, use the ID as string
|
||||
return name
|
||||
return number
|
||||
|
||||
def get_num_from_short_name(short_name, nodeInt=1):
|
||||
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
|
||||
logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}")
|
||||
if nodeInt == 1:
|
||||
for node in interface1.nodes.values():
|
||||
#logger.debug(f"System: Checking Node: {node['user']['shortName']} against {short_name} for number {node['num']}")
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
else:
|
||||
# try other interface
|
||||
if interface2_enabled:
|
||||
for node in interface2.nodes.values():
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
if nodeInt == 2:
|
||||
for node in interface2.nodes.values():
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
else:
|
||||
# try other interface
|
||||
if interface2_enabled:
|
||||
for node in interface1.nodes.values():
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
return 0
|
||||
|
||||
def get_node_list(nodeInt=1):
|
||||
# Get a list of nodes on the device
|
||||
@@ -156,6 +230,7 @@ def get_node_list(nodeInt=1):
|
||||
node_list1 = []
|
||||
node_list2 = []
|
||||
short_node_list = []
|
||||
last_heard = 0
|
||||
if nodeInt == 1:
|
||||
if interface1.nodes:
|
||||
for node in interface1.nodes.values():
|
||||
@@ -171,7 +246,7 @@ def get_node_list(nodeInt=1):
|
||||
item = (node_name, last_heard, snr)
|
||||
node_list1.append(item)
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No nodes found")
|
||||
logger.warning(f"System: No nodes found")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
if nodeInt == 2:
|
||||
@@ -189,30 +264,44 @@ def get_node_list(nodeInt=1):
|
||||
item = (node_name, last_heard, snr)
|
||||
node_list2.append(item)
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No nodes found")
|
||||
logger.warning(f"System: No nodes found")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
node_list1.sort(key=lambda x: x[1], reverse=True)
|
||||
#print (f"Node List: {node_list1[:5]}\n")
|
||||
node_list2.sort(key=lambda x: x[1], reverse=True)
|
||||
try:
|
||||
#print (f"Node List: {node_list1[:5]}\n")
|
||||
node_list1.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
|
||||
#print (f"Node List: {node_list1[:5]}\n")
|
||||
if interface2_enabled:
|
||||
node_list2.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error sorting node list: {e}")
|
||||
logger.debug(f"Node List1: {node_list1[:5]}\n")
|
||||
if interface2_enabled:
|
||||
logger.debug(f"Node List2: {node_list2[:5]}\n")
|
||||
node_list = ERROR_FETCHING_DATA
|
||||
|
||||
# make a nice list for the user
|
||||
for x in node_list1[:SITREP_NODE_COUNT]:
|
||||
short_node_list.append(f"{x[0]} SNR:{x[2]}")
|
||||
for x in node_list2[:SITREP_NODE_COUNT]:
|
||||
short_node_list.append(f"{x[0]} SNR:{x[2]}")
|
||||
try:
|
||||
# make a nice list for the user
|
||||
for x in node_list1[:SITREP_NODE_COUNT]:
|
||||
short_node_list.append(f"{x[0]} SNR:{x[2]}")
|
||||
for x in node_list2[:SITREP_NODE_COUNT]:
|
||||
short_node_list.append(f"{x[0]} SNR:{x[2]}")
|
||||
|
||||
for x in short_node_list:
|
||||
if x != "" or x != '\n':
|
||||
node_list += x + "\n"
|
||||
for x in short_node_list:
|
||||
if x != "" or x != '\n':
|
||||
node_list += x + "\n"
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error creating node list: {e}")
|
||||
node_list = ERROR_FETCHING_DATA
|
||||
|
||||
return node_list
|
||||
|
||||
def get_node_location(number, nodeInt=1):
|
||||
def get_node_location(number, nodeInt=1, channel=0):
|
||||
# Get the location of a node by its number from nodeDB on device
|
||||
latitude = latitudeValue
|
||||
longitude = longitudeValue
|
||||
position = [latitudeValue,longitudeValue]
|
||||
lastheard = 0
|
||||
if nodeInt == 1:
|
||||
if interface1.nodes:
|
||||
for node in interface1.nodes.values():
|
||||
@@ -222,15 +311,24 @@ def get_node_location(number, nodeInt=1):
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
except Exception as e:
|
||||
print (f"{log_timestamp()} System: Error getting location data for {number}")
|
||||
print (f"System: location data for {number} is {latitude},{longitude}")
|
||||
logger.error(f"System: Error getting location data for {number}")
|
||||
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
return position
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No location data for {number}")
|
||||
logger.warning(f"System: No location data for {number} using default location")
|
||||
# request location data
|
||||
# try:
|
||||
# logger.debug(f"System: Requesting location data for {number}")
|
||||
# if nodeInt == 1:
|
||||
# interface1.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
|
||||
# if nodeInt == 2:
|
||||
# interface2.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
|
||||
# except Exception as e:
|
||||
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
|
||||
return position
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No nodes found")
|
||||
logger.warning(f"System: No nodes found")
|
||||
return position
|
||||
if nodeInt == 2:
|
||||
if interface2.nodes:
|
||||
@@ -241,21 +339,100 @@ def get_node_location(number, nodeInt=1):
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
except Exception as e:
|
||||
print (f"{log_timestamp()} System: Error getting location data for {number}")
|
||||
print (f"System: location data for {number} is {latitude},{longitude}")
|
||||
logger.error(f"System: Error getting location data for {number}")
|
||||
logger.info(f"System: location data for {number} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
return position
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No location data for {number}")
|
||||
logger.warning(f"System: No location data for {number}")
|
||||
return position
|
||||
else:
|
||||
print (f"{log_timestamp()} System: No nodes found")
|
||||
logger.warning(f"System: No nodes found")
|
||||
return position
|
||||
return position
|
||||
|
||||
def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
node_list = []
|
||||
|
||||
if nodeInt == 1:
|
||||
if interface1.nodes:
|
||||
for node in interface1.nodes.values():
|
||||
if 'position' in node:
|
||||
try:
|
||||
nodeID = node['num']
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
|
||||
#lastheard time in unix time
|
||||
lastheard = node.get('lastHeard', 0)
|
||||
#if last heard is over 24 hours ago, ignore the node
|
||||
if lastheard < (time.time() - 86400):
|
||||
continue
|
||||
|
||||
# Calculate distance to node from config.ini location
|
||||
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
|
||||
|
||||
if (distance < sentry_radius):
|
||||
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
|
||||
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
# else:
|
||||
# # request location data
|
||||
# try:
|
||||
# logger.debug(f"System: Requesting location data for {node['id']}")
|
||||
# interface1.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=publicChannel)
|
||||
# except Exception as e:
|
||||
# logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
|
||||
|
||||
# sort by distance closest
|
||||
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
|
||||
node_list.sort(key=lambda x: x['distance'])
|
||||
# return the first 3 closest nodes by default
|
||||
return node_list[:returnCount]
|
||||
else:
|
||||
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
if nodeInt == 2:
|
||||
if interface2.nodes:
|
||||
for node in interface2.nodes.values():
|
||||
if 'position' in node:
|
||||
try:
|
||||
nodeID = node['num']
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
|
||||
#lastheard time in unix time
|
||||
lastheard = node.get('lastHeard', 0)
|
||||
#if last heard is over 24 hours ago, ignore the node
|
||||
if lastheard < (time.time() - 86400):
|
||||
continue
|
||||
|
||||
# Calculate distance to node from config.ini location
|
||||
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
|
||||
|
||||
if (distance < sentry_radius):
|
||||
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
|
||||
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
# sort by distance closest
|
||||
node_list.sort(key=lambda x: x['distance'])
|
||||
# return the first 3 closest nodes by default
|
||||
return node_list[:returnCount]
|
||||
else:
|
||||
logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
if message == "" or message == None or len(message) == 0:
|
||||
return
|
||||
# if message over MESSAGE_CHUNK_SIZE characters, split it into multiple messages
|
||||
if len(message) > MESSAGE_CHUNK_SIZE:
|
||||
print (f"{log_timestamp()} System: Splitting Message, Message Length: {len(message)}")
|
||||
logger.debug(f"System: Splitting Message, Message Length: {len(message)}")
|
||||
|
||||
# split the message into MESSAGE_CHUNK_SIZE 160 character chunks
|
||||
message = message.replace('\n', ' NEWLINE ') # replace newlines with NEWLINE to keep them in split chunks
|
||||
@@ -266,7 +443,7 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
|
||||
for word in split_message:
|
||||
if len(line + word) < MESSAGE_CHUNK_SIZE:
|
||||
if word == 'NEWLINE':
|
||||
if 'NEWLINE' in word or '\n' in word or '\r' in word:
|
||||
# chunk by newline if it exists
|
||||
message_list.append(line)
|
||||
line = ''
|
||||
@@ -277,18 +454,20 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
line = word + ' '
|
||||
|
||||
message_list.append(line) # needed add contents of the last 'line' into the list
|
||||
message_list = [m.replace('NEWLINE', '') for m in message_list]
|
||||
|
||||
for m in message_list:
|
||||
if nodeid == 0:
|
||||
#Send to channel
|
||||
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Multi-Chunk Message: {m}")
|
||||
# Send to channel
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
if nodeInt == 1:
|
||||
interface1.sendText(text=m, channelIndex=ch)
|
||||
if nodeInt == 2:
|
||||
interface2.sendText(text=m, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} Multi-Chunk Message: {m} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
if nodeInt == 1:
|
||||
interface1.sendText(text=m, channelIndex=ch, destinationId=nodeid)
|
||||
if nodeInt == 2:
|
||||
@@ -296,14 +475,15 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
else: # message is less than MESSAGE_CHUNK_SIZE characters
|
||||
if nodeid == 0:
|
||||
# Send to channel
|
||||
print (f"{log_timestamp()} System: Sending Device:{nodeInt} Channel:{ch} Message: {message}")
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
if nodeInt == 1:
|
||||
interface1.sendText(text=message, channelIndex=ch)
|
||||
if nodeInt == 2:
|
||||
interface2.sendText(text=message, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
print (f"{log_timestamp()} System: Sending DM Device:{nodeInt} {message} To: {get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
if nodeInt == 1:
|
||||
interface1.sendText(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
if nodeInt == 2:
|
||||
@@ -317,14 +497,16 @@ 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):
|
||||
# search wikipedia for a summary of the search term
|
||||
try:
|
||||
logger.debug(f"System: Searching Wikipedia for:{search_term}")
|
||||
summary = wikipedia.summary(search_term, sentences=wiki_return_limit)
|
||||
return summary
|
||||
except Exception as e:
|
||||
# The errors are vebose, normallly around trying to guess the search term
|
||||
logger.warning(f"System: Error searching Wikipedia for:{search_term}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def messageTrap(msg):
|
||||
# Check if the message contains a trap word
|
||||
@@ -337,52 +519,57 @@ def messageTrap(msg):
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
print(f"\n{log_timestamp()} System: Closing Autoresponder\n")
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
interface1.close()
|
||||
print(f"{log_timestamp()} System: Interface1 Closed")
|
||||
logger.debug(f"System: Interface1 Closed")
|
||||
if interface2_enabled:
|
||||
interface2.close()
|
||||
print(f"{log_timestamp()} System: Interface2 Closed")
|
||||
logger.debug(f"System: Interface2 Closed")
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error closing: {e}")
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
save_bbsdb()
|
||||
save_bbsdm()
|
||||
print(f"{log_timestamp()} System: BBS Messages Saved")
|
||||
print(f"{log_timestamp()} System: Exiting")
|
||||
logger.debug(f"System: BBS Messages Saved")
|
||||
logger.debug(f"System: Exiting")
|
||||
asyncLoop.stop()
|
||||
asyncLoop.close()
|
||||
exit (0)
|
||||
|
||||
async def BroadcastScheduler():
|
||||
# handle schedule checks for the broadcast of messages
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def handleSignalWatcher():
|
||||
global lastHamLibAlert, antiSpam, sigWatchBrodcastCh
|
||||
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
|
||||
# monitor rigctld for signal strength and frequency
|
||||
while True:
|
||||
msg = await signalWatcher()
|
||||
if msg != ERROR_FETCHING_DATA and msg is not None:
|
||||
print(f"{log_timestamp()} System: Detected Alert from Hamlib {msg}")
|
||||
logger.debug(f"System: Detected Alert from Hamlib {msg}")
|
||||
|
||||
# check we are not spammig the channel limit messages to once per minute
|
||||
if time.time() - lastHamLibAlert > 60:
|
||||
lastHamLibAlert = time.time()
|
||||
# if sigWatchBrodcastCh list contains multiple channels, broadcast to all
|
||||
if type(sigWatchBrodcastCh) is list:
|
||||
for ch in sigWatchBrodcastCh:
|
||||
if type(sigWatchBroadcastCh) is list:
|
||||
for ch in sigWatchBroadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(ch), 0, 2)
|
||||
else:
|
||||
print(f"{log_timestamp()} System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
else:
|
||||
if antiSpam and sigWatchBrodcastCh != publicChannel:
|
||||
send_message(msg, int(sigWatchBrodcastCh), 0, 1)
|
||||
if antiSpam and sigWatchBroadcastCh != publicChannel:
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(sigWatchBrodcastCh), 0, 2)
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, 2)
|
||||
else:
|
||||
print(f"{log_timestamp()} System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
pass
|
||||
@@ -398,7 +585,7 @@ async def retry_interface(nodeID=1):
|
||||
try:
|
||||
interface1.close()
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error closing interface1: {e}")
|
||||
logger.error(f"System: closing interface1: {e}")
|
||||
if nodeID==2:
|
||||
if interface2 is not None:
|
||||
retry_int2 = True
|
||||
@@ -406,15 +593,15 @@ async def retry_interface(nodeID=1):
|
||||
try:
|
||||
interface2.close()
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error closing interface2: {e}")
|
||||
logger.error(f"System: closing interface2: {e}")
|
||||
|
||||
|
||||
print(f"{log_timestamp()} System: Retrying interface in 15 seconds")
|
||||
logger.debug(f"System: Retrying interface in 15 seconds")
|
||||
if max_retry_count1 == 0:
|
||||
print(f"{log_timestamp()} System: Max retry count reached for interface1")
|
||||
logger.critical(f"System: Max retry count reached for interface1")
|
||||
exit_handler()
|
||||
if max_retry_count2 == 0:
|
||||
print(f"{log_timestamp()} System: Max retry count reached for interface2")
|
||||
logger.critical(f"System: Max retry count reached for interface2")
|
||||
exit_handler()
|
||||
# wait 15 seconds before retrying
|
||||
await asyncio.sleep(15)
|
||||
@@ -423,82 +610,125 @@ async def retry_interface(nodeID=1):
|
||||
try:
|
||||
if nodeID==1 and retry_int1:
|
||||
interface1 = None
|
||||
print(f"{log_timestamp()} System: Retrying Interface1")
|
||||
logger.debug(f"System: Retrying Interface1")
|
||||
if interface1_type == 'serial':
|
||||
interface1 = meshtastic.serial_interface.SerialInterface(port1)
|
||||
elif interface1_type == 'tcp':
|
||||
interface1 = meshtastic.tcp_interface.TCPInterface(hostname1)
|
||||
elif interface1_type == 'ble':
|
||||
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
|
||||
print(f"{log_timestamp()} System: Interface1 Opened!")
|
||||
logger.debug(f"System: Interface1 Opened!")
|
||||
retry_int1 = False
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error opening interface1 on: {e}")
|
||||
logger.error(f"System: opening interface1 on: {e}")
|
||||
|
||||
try:
|
||||
if nodeID==2 and retry_int2:
|
||||
interface2 = None
|
||||
print(f"{log_timestamp()} System: Retrying Interface2")
|
||||
logger.debug(f"System: Retrying Interface2")
|
||||
if interface2_type == 'serial':
|
||||
interface2 = meshtastic.serial_interface.SerialInterface(port2)
|
||||
elif interface2_type == 'tcp':
|
||||
interface2 = meshtastic.tcp_interface.TCPInterface(hostname2)
|
||||
elif interface2_type == 'ble':
|
||||
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
|
||||
print(f"{log_timestamp()} System: Interface2 Opened!")
|
||||
logger.debug(f"System: Interface2 Opened!")
|
||||
retry_int2 = False
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error opening interface2: {e}")
|
||||
|
||||
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import sys
|
||||
|
||||
@contextmanager
|
||||
def suppress_stdout():
|
||||
with open(os.devnull, "w") as devnull:
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = devnull
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
logger.error(f"System: opening interface2: {e}")
|
||||
|
||||
async def watchdog():
|
||||
global retry_int1, retry_int2
|
||||
if sentry_enabled:
|
||||
sentry_loop = 0
|
||||
lastSpotted = ""
|
||||
enemySpotted = ""
|
||||
sentry_loop2 = 0
|
||||
lastSpotted2 = ""
|
||||
enemySpotted2 = ""
|
||||
# watchdog for connection to the interface
|
||||
while True:
|
||||
await asyncio.sleep(20)
|
||||
#print(f"{log_timestamp()} System: watchdog running\r", end="")
|
||||
#print(f"MeshBot System: watchdog running\r", end="")
|
||||
if interface1 is not None and not retry_int1:
|
||||
try:
|
||||
with suppress_stdout():
|
||||
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
|
||||
with contextlib.redirect_stdout(None):
|
||||
interface1.localNode.getMetadata()
|
||||
print(f"System: if you see this upgrade python to >3.4")
|
||||
#if "device_state_version:" not in meta:
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error communicating with interface1, trying to reconnect: {e}")
|
||||
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
|
||||
retry_int1 = True
|
||||
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
if sentry_enabled:
|
||||
try:
|
||||
closest_nodes1 = get_closest_nodes(1)
|
||||
if closest_nodes1 != ERROR_FETCHING_DATA:
|
||||
if closest_nodes1[0]['id'] is not None:
|
||||
enemySpotted = get_name_from_number(closest_nodes1[0]['id'], 'long', 1)
|
||||
enemySpotted += ", " + get_name_from_number(closest_nodes1[0]['id'], 'short', 1)
|
||||
enemySpotted += ", " + str(closest_nodes1[0]['id'])
|
||||
enemySpotted += ", " + decimal_to_hex(closest_nodes1[0]['id'])
|
||||
enemySpotted += f" at {closest_nodes1[0]['distance']}m"
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if sentry_loop >= sentry_holdoff and lastSpotted != enemySpotted:
|
||||
logger.warning(f"System: {enemySpotted} is close to your location on Interface1")
|
||||
send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 1)
|
||||
if interface2_enabled:
|
||||
await asyncio.sleep(1.5)
|
||||
send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 2)
|
||||
sentry_loop = 0
|
||||
lastSpotted = enemySpotted
|
||||
else:
|
||||
sentry_loop += 1
|
||||
|
||||
if retry_int1:
|
||||
try:
|
||||
await retry_interface(1)
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error retrying interface1: {e}")
|
||||
logger.error(f"System: retrying interface1: {e}")
|
||||
|
||||
if interface2_enabled:
|
||||
if interface2 is not None and not retry_int2:
|
||||
try:
|
||||
with suppress_stdout():
|
||||
with contextlib.redirect_stdout(None):
|
||||
interface2.localNode.getMetadata()
|
||||
print(f"System: if you see this upgrade python to >3.4")
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error communicating with interface2, trying to reconnect: {e}")
|
||||
logger.error(f"System: communicating with interface2, trying to reconnect: {e}")
|
||||
retry_int2 = True
|
||||
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
if sentry_enabled:
|
||||
try:
|
||||
closest_nodes2 = get_closest_nodes(2)
|
||||
if closest_nodes2 != ERROR_FETCHING_DATA:
|
||||
if closest_nodes2[0]['id'] is not None:
|
||||
enemySpotted2 = get_name_from_number(closest_nodes2[0]['id'], 'long', 2)
|
||||
enemySpotted2 += ", " + get_name_from_number(closest_nodes2[0]['id'], 'short', 2)
|
||||
enemySpotted2 += ", " + str(closest_nodes2[0]['id'])
|
||||
enemySpotted2 += ", " + decimal_to_hex(closest_nodes2[0]['id'])
|
||||
enemySpotted2 += f" at {closest_nodes2[0]['distance']}m"
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if sentry_loop2 >= sentry_holdoff and lastSpotted2 != enemySpotted2:
|
||||
logger.warning(f"System: {enemySpotted2} is close to your location on Interface2")
|
||||
# send to secure channel on both interfaces
|
||||
send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 1)
|
||||
await asyncio.sleep(1.5)
|
||||
send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 2)
|
||||
sentry_loop2 = 0
|
||||
lastSpotted2 = enemySpotted2
|
||||
else:
|
||||
sentry_loop2 += 1
|
||||
|
||||
if retry_int2:
|
||||
try:
|
||||
await retry_interface(2)
|
||||
except Exception as e:
|
||||
print(f"{log_timestamp()} System: Error retrying interface2: {e}")
|
||||
|
||||
logger.error(f"System: retrying interface2: {e}")
|
||||
|
||||
177
modules/wx_meteo.py
Normal file
177
modules/wx_meteo.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import openmeteo_requests # pip install openmeteo-requests
|
||||
from retry_requests import retry # pip install retry_requests
|
||||
#import requests_cache
|
||||
from modules.log import *
|
||||
|
||||
def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Setup the Open-Meteo API client with cache and retry on error
|
||||
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||
retry_session = retry(retries = 3, backoff_factor = 0.2)
|
||||
openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||
|
||||
# Make sure all required weather variables are listed here
|
||||
# The order of variables in hourly or daily is important to assign them correctly below
|
||||
url = "https://api.open-meteo.com/v1/forecast"
|
||||
params = {
|
||||
"latitude": {lat},
|
||||
"longitude": {lon},
|
||||
"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_hours", "precipitation_probability_max", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant"],
|
||||
"timezone": "auto",
|
||||
"forecast_days": {forecastDays}
|
||||
}
|
||||
|
||||
# Unit 0 is imperial, 1 is metric
|
||||
if unit == 0:
|
||||
params["temperature_unit"] = "fahrenheit"
|
||||
params["wind_speed_unit"] = "mph"
|
||||
params["precipitation_unit"] = "inch"
|
||||
params["distance_unit"] = "mile"
|
||||
params["pressure_unit"] = "inHg"
|
||||
|
||||
try:
|
||||
# Fetch the weather data
|
||||
responses = openmeteo.weather_api(url, params=params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
response = responses[0]
|
||||
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response.Daily()
|
||||
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
|
||||
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
|
||||
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
|
||||
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
|
||||
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
|
||||
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
|
||||
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# convert wind value to cardinal directions
|
||||
for value in daily_wind_direction_10m_dominant:
|
||||
if value < 22.5:
|
||||
wind_direction = "N"
|
||||
elif value < 67.5:
|
||||
wind_direction = "NE"
|
||||
elif value < 112.5:
|
||||
wind_direction = "E"
|
||||
elif value < 157.5:
|
||||
wind_direction = "SE"
|
||||
elif value < 202.5:
|
||||
wind_direction = "S"
|
||||
elif value < 247.5:
|
||||
wind_direction = "SW"
|
||||
elif value < 292.5:
|
||||
wind_direction = "W"
|
||||
elif value < 337.5:
|
||||
wind_direction = "NW"
|
||||
else:
|
||||
wind_direction = "N"
|
||||
|
||||
# create a weather report
|
||||
weather_report = ""
|
||||
for i in range(forecastDays):
|
||||
if str(i + 1) == "1":
|
||||
weather_report += "Today, "
|
||||
elif str(i + 1) == "2":
|
||||
weather_report += "Tomorrow, "
|
||||
else:
|
||||
weather_report += "Futurecast: "
|
||||
|
||||
# report weather from WMO Weather interpretation codes (WW)
|
||||
code_string = ""
|
||||
if daily_weather_code[i] == 0:
|
||||
code_string = "Clear sky"
|
||||
elif daily_weather_code[i] == 1 or 2 or 3:
|
||||
code_string = "Partly cloudy"
|
||||
elif daily_weather_code[i] == 45 or 48:
|
||||
code_string = "Fog"
|
||||
elif daily_weather_code[i] == 51:
|
||||
code_string = "Drizzle: Light"
|
||||
elif daily_weather_code[i] == 53:
|
||||
code_string = "Drizzle: Moderate"
|
||||
elif daily_weather_code[i] == 55:
|
||||
code_string = "Drizzle: Heavy"
|
||||
elif daily_weather_code[i] == 56:
|
||||
code_string = "Freezing Drizzle: Light"
|
||||
elif daily_weather_code[i] == 57:
|
||||
code_string = "Freezing Drizzle: Moderate"
|
||||
elif daily_weather_code[i] == 61:
|
||||
code_string = "Rain: Slight"
|
||||
elif daily_weather_code[i] == 63:
|
||||
code_string = "Rain: Moderate"
|
||||
elif daily_weather_code[i] == 65:
|
||||
code_string = "Rain: Heavy"
|
||||
elif daily_weather_code[i] == 66:
|
||||
code_string = "Freezing Rain: Light"
|
||||
elif daily_weather_code[i] == 67:
|
||||
code_string = "Freezing Rain: Dense"
|
||||
elif daily_weather_code[i] == 71:
|
||||
code_string = "Snow: Light"
|
||||
elif daily_weather_code[i] == 73:
|
||||
code_string = "Snow: Moderate"
|
||||
elif daily_weather_code[i] == 75:
|
||||
code_string = "Snow: Heavy"
|
||||
elif daily_weather_code[i] == 77:
|
||||
code_string = "Snow Grains"
|
||||
elif daily_weather_code[i] == 80:
|
||||
code_string = "Rain showers: Slight"
|
||||
elif daily_weather_code[i] == 81:
|
||||
code_string = "Rain showers: Moderate"
|
||||
elif daily_weather_code[i] == 82:
|
||||
code_string = "Rain showers: Heavy"
|
||||
elif daily_weather_code[i] == 85:
|
||||
code_string = "Snow showers: Light"
|
||||
elif daily_weather_code[i] == 86:
|
||||
code_string = "Snow showers: Moderate"
|
||||
elif daily_weather_code[i] == 95:
|
||||
code_string = "Thunderstorm: Slight"
|
||||
elif daily_weather_code[i] == 96:
|
||||
code_string = "Thunderstorm: Moderate"
|
||||
elif daily_weather_code[i] == 99:
|
||||
code_string = "Thunderstorm: Heavy"
|
||||
|
||||
weather_report += "Cond: " + code_string + ". "
|
||||
|
||||
# report temperature
|
||||
if unit == 0:
|
||||
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "F, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "F. "
|
||||
else:
|
||||
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "C, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "C. "
|
||||
|
||||
# check for precipitation
|
||||
if daily_precipitation_hours[i] > 0:
|
||||
if unit == 0:
|
||||
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "in, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
|
||||
else:
|
||||
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "mm, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
|
||||
else:
|
||||
weather_report += "No Precip. "
|
||||
|
||||
# check for wind
|
||||
if daily_wind_speed_10m_max[i] > 0:
|
||||
if unit == 0:
|
||||
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "mph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "mph from:" + wind_direction + "."
|
||||
else:
|
||||
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "kph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "kph from:" + wind_direction + "."
|
||||
else:
|
||||
weather_report += "No Wind\n"
|
||||
|
||||
# add a new line for the next day
|
||||
if i < forecastDays - 1:
|
||||
weather_report += "\n"
|
||||
|
||||
return weather_report
|
||||
|
||||
214
pong_bot.py
214
pong_bot.py
@@ -5,59 +5,112 @@
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from pubsub import pub # pip install pubsub
|
||||
from modules.settings import *
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
|
||||
# Auto response to messages
|
||||
if "ping" in message.lower():
|
||||
# Check if the user added @foo to the message
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
bot_response = "🏓PONG, " + hop
|
||||
elif "ack" in message.lower():
|
||||
if hop == "Direct":
|
||||
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
bot_response = "🏓ACK-ACK! " + hop
|
||||
elif "pong" in message.lower():
|
||||
bot_response = "🏓Ping!!"
|
||||
elif "motd" in message.lower():
|
||||
# check if the user wants to set the motd by using $
|
||||
if "$" in message:
|
||||
motd = message.split("$")[1]
|
||||
global MOTD
|
||||
MOTD = motd
|
||||
bot_response = "MOTD Set to: " + MOTD
|
||||
else:
|
||||
bot_response = MOTD
|
||||
elif "cmd" in message.lower() or "cmd?" in message.lower():
|
||||
bot_response = help_message
|
||||
elif "lheard" in message.lower() or "sitrep" in message.lower():
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
elif "testing" in message.lower() or "test" in message.lower():
|
||||
bot_response = "🏓Testing 1,2,3"
|
||||
else:
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
message_lower = message.lower()
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
"ping": lambda: handle_ping(message, hop, snr, rssi),
|
||||
"pong": lambda: "🏓Ping!!",
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"ack": lambda: handle_ack(hop, snr, rssi),
|
||||
"testing": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_testing(hop, snr, rssi),
|
||||
}
|
||||
cmds = [] # list to hold the commands found in the message
|
||||
for key in command_handler:
|
||||
if key in message_lower.split(' '):
|
||||
cmds.append({'cmd': key, 'index': message_lower.index(key)})
|
||||
|
||||
if len(cmds) > 0:
|
||||
# sort the commands by index value
|
||||
cmds = sorted(cmds, key=lambda k: k['index'])
|
||||
logger.debug(f"System: Bot detected Commands:{cmds}")
|
||||
# run the first command after sorting
|
||||
bot_response = command_handler[cmds[0]['cmd']]()
|
||||
|
||||
# wait a 700ms to avoid message collision from lora-ack
|
||||
time.sleep(0.7)
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_ping(message, hop, snr, rssi):
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓PONG, " + hop
|
||||
|
||||
def handle_motd(message):
|
||||
global MOTD
|
||||
if "$" in message:
|
||||
motd = message.split("$")[1]
|
||||
MOTD = motd.rstrip()
|
||||
return "MOTD Set to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
return bot_response
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
rxType = type(interface).__name__
|
||||
@@ -79,6 +132,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
|
||||
@@ -131,7 +190,7 @@ def onReceive(packet, interface):
|
||||
|
||||
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
print(f"{log_timestamp()} Got Own Welcome/Help header. Device:{rxNode} From: {get_name_from_number(message_from_id)}")
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
return
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
@@ -140,25 +199,29 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if messageTrap(message_string):
|
||||
print(f"{log_timestamp()} Received DM: {message_string} on Device:{rxNode} Channel: {channel_number} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
print(f"{log_timestamp()} Ignoring DM: {message_string} on Device:{rxNode} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
print(f"{log_timestamp()} Received On Device:{rxNode} Channel {channel_number}: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel:
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
print(f"{log_timestamp()} System: Warning spamming default channel not allowed. sending DM to {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
@@ -166,8 +229,8 @@ def onReceive(packet, interface):
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
|
||||
else:
|
||||
# ignore the message but add it to the message history and repeat it if enabled
|
||||
# add the message to the message history but limit
|
||||
# message is not for bot to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
@@ -178,40 +241,53 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
msg_history.pop(0)
|
||||
msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode))
|
||||
|
||||
# check if repeater is enabled and the other interface is enabled
|
||||
|
||||
# print the message to the log and sdout
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
|
||||
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
# repeat the message on the other device
|
||||
# wait a 700ms to avoid message collision from lora-ack.
|
||||
time.sleep(0.7)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
# wait a 700ms to avoid message collision from lora-ack
|
||||
time.sleep(0.7)
|
||||
if str(channel_number) in repeater_channels:
|
||||
if rxNode == 1:
|
||||
print(f"{log_timestamp()} Repeating message on Device2 Channel:{channel_number}")
|
||||
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 2)
|
||||
elif rxNode == 2:
|
||||
print(f"{log_timestamp()} Repeating message on Device1 Channel:{channel_number}")
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
print(f"{log_timestamp()} System: Ignoring incoming Device:{rxNode} Channel:{channel_number} Message: {message_string} From: {get_name_from_number(message_from_id)}")
|
||||
except KeyError as e:
|
||||
print(f"{log_timestamp()} System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
print(packet) # print the packet for debugging
|
||||
print("END of packet \n")
|
||||
|
||||
async def start_rx():
|
||||
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
|
||||
msg = (f"{log_timestamp()} System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
print (msg)
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
msg = (f"{log_timestamp()} System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
print (msg)
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
if log_messages_to_file:
|
||||
logger.debug(f"System: Logging Messages to disk")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Enabled")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
|
||||
@@ -7,3 +7,13 @@ geopy
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
langchain
|
||||
langchain-ollama
|
||||
ollama
|
||||
|
||||
|
||||
Reference in New Issue
Block a user