Compare commits

...

85 Commits

Author SHA1 Message Date
SpudGunMan fe1854f2d8 Update system.py 2024-08-13 15:22:38 -07:00
SpudGunMan df9a34dc16 Update system.py 2024-08-13 15:02:40 -07:00
SpudGunMan e762ea4b90 Update install.sh 2024-08-13 14:23:49 -07:00
SpudGunMan 3b725837ac fixes 2024-08-13 13:57:12 -07:00
SpudGunMan 23efd8e5d8 Update mesh_bot.py 2024-08-13 13:50:53 -07:00
SpudGunMan b61463f570 Update mesh_bot.py 2024-08-13 13:40:08 -07:00
SpudGunMan 8339233459 Update install.sh 2024-08-13 13:35:30 -07:00
SpudGunMan df68111f0c Update config.template 2024-08-13 13:34:33 -07:00
SpudGunMan b73ad38156 Update install.sh
reference https://github.com/SpudGunMan/meshing-around/issues/37
2024-08-13 00:14:50 -07:00
SpudGunMan 2b7d1ed09f Update README.md 2024-08-13 00:00:16 -07:00
SpudGunMan f1ef5fa787 cleanup 2024-08-12 23:49:50 -07:00
Kelly ec14e07513 Merge pull request #39 from SpudGunMan/case_test
refactor autoresponse logic
2024-08-12 11:52:50 -07:00
SpudGunMan efdd5fab66 enhance 2024-08-12 11:40:55 -07:00
SpudGunMan 4fa114a3f2 fix 2024-08-12 03:11:10 -07:00
SpudGunMan ab64ff14b1 Update mesh_bot.py 2024-08-12 02:59:27 -07:00
SpudGunMan 65609c5822 Update mesh_bot.py 2024-08-12 02:57:34 -07:00
SpudGunMan bdd41c0434 Update mesh_bot.py 2024-08-12 02:54:32 -07:00
SpudGunMan 80da793c8d Update mesh_bot.py 2024-08-12 02:53:24 -07:00
SpudGunMan ba6c296b14 Update mesh_bot.py 2024-08-12 02:52:38 -07:00
SpudGunMan 9ae95752ad Update mesh_bot.py 2024-08-12 02:51:34 -07:00
SpudGunMan 9ba430c53c enhance 2024-08-12 02:36:53 -07:00
SpudGunMan 9e605a2717 Update settings.py 2024-08-12 01:23:52 -07:00
SpudGunMan aeab22010f typo 2024-08-12 00:54:19 -07:00
SpudGunMan 2d20f4479c fixMOTD and settings 2024-08-12 00:43:10 -07:00
SpudGunMan 6546679def rearrange auto if 2024-08-11 23:47:52 -07:00
Kelly 4dabd20a2e Merge pull request #38 from SpudGunMan/sentry
Sentry Mode
2024-08-11 23:04:33 -07:00
SpudGunMan d8e5cb7893 Update config.template 2024-08-11 23:03:48 -07:00
SpudGunMan 28514adf00 enhance 2024-08-11 23:02:19 -07:00
SpudGunMan bfa8aa0a86 enhance
fix some issues raised https://github.com/SpudGunMan/meshing-around/issues/37 thankyou!
2024-08-10 12:37:22 -07:00
SpudGunMan 9e205155a5 Update system.py 2024-08-10 09:58:30 -07:00
SpudGunMan 1e921dd5ea geopy tired of maths 2024-08-10 01:48:12 -07:00
SpudGunMan 5c73e49610 Update mesh_bot.py 2024-08-10 00:25:01 -07:00
SpudGunMan 91f11e4828 enhance 2024-08-10 00:17:24 -07:00
SpudGunMan 4a9c969dc0 Update mesh_bot.py 2024-08-10 00:04:42 -07:00
SpudGunMan 88e960ae33 cant handle 🥔 2024-08-10 00:03:08 -07:00
Kelly 0217f4f2cc Merge pull request #35 from SpudGunMan/main
PullinMain
2024-08-09 23:45:56 -07:00
SpudGunMan 29fb8b0b40 Update mesh_bot.py 2024-08-09 23:22:57 -07:00
SpudGunMan 773ee78fb2 Update mesh_bot.py 2024-08-09 23:22:24 -07:00
SpudGunMan d43e28d723 Update README.md 2024-08-09 23:21:50 -07:00
SpudGunMan d063fdd81d Update README.md 2024-08-09 23:21:13 -07:00
SpudGunMan f73cd5ec31 cleanup 2024-08-09 23:19:30 -07:00
SpudGunMan 35df43b727 bbspost by shortname
this is very basic for now
2024-08-09 23:09:21 -07:00
SpudGunMan e17999a2d6 numpy.. 2024-08-08 23:04:51 -07:00
SpudGunMan 9f658fc060 Update wx_meteo.py 2024-08-08 22:52:04 -07:00
SpudGunMan 27ece919d7 Update system.py 2024-08-08 12:41:29 -07:00
SpudGunMan 0e97953adf Update system.py 2024-08-08 12:32:17 -07:00
SpudGunMan 66d44c3a6d ignorelist 2024-08-08 12:21:55 -07:00
Kelly 66ca1b4103 Merge pull request #34 from SpudGunMan/main
fix depends
2024-08-08 11:54:08 -07:00
SpudGunMan 0b3040f7b7 fix depends 2024-08-08 11:53:27 -07:00
SpudGunMan 066f7edfd9 Revert "fix depends"
This reverts commit 72f049452b.
2024-08-08 11:52:13 -07:00
SpudGunMan 72f049452b fix depends 2024-08-08 11:51:50 -07:00
SpudGunMan c1b493b7c7 Update system.py 2024-08-08 03:35:43 -07:00
SpudGunMan 67af1ba39e sentry notification 2024-08-08 02:57:52 -07:00
SpudGunMan c48851719a Update system.py 2024-08-08 02:53:13 -07:00
SpudGunMan cfbda17cfb Update system.py 2024-08-08 02:06:52 -07:00
SpudGunMan be32fd4a17 Update system.py 2024-08-08 02:04:21 -07:00
SpudGunMan 98b9e0471c Update README.md 2024-08-08 02:00:39 -07:00
SpudGunMan 9efbbb4f20 Update system.py 2024-08-08 02:00:24 -07:00
SpudGunMan 7b8779fc48 Update system.py 2024-08-08 01:58:39 -07:00
SpudGunMan 07e6042e67 Update system.py 2024-08-08 01:55:55 -07:00
SpudGunMan 814303c521 fix 2024-08-08 01:47:00 -07:00
SpudGunMan 2673b638bf newidea 2024-08-08 01:42:27 -07:00
SpudGunMan 92b7b7ae2a cleanup messageLog 2024-08-08 00:11:12 -07:00
SpudGunMan 7d63c2dc11 tidy output for not printing \n 2024-08-07 22:56:36 -07:00
SpudGunMan 514facacd5 cleanup 2024-08-07 22:46:30 -07:00
SpudGunMan 89dc8791d0 fix debug 2024-08-07 21:55:36 -07:00
SpudGunMan 700f65ce73 Update system.py
resolution for https://github.com/SpudGunMan/meshing-around/issues/31
2024-08-07 21:53:36 -07:00
SpudGunMan 4f24701460 Update locationdata.py 2024-08-07 21:26:54 -07:00
SpudGunMan 0514d51aea cleanup 2024-08-07 21:24:35 -07:00
Kelly 99a05c66ef Merge pull request #33 from SpudGunMan/worldwx
World Weather
2024-08-07 20:08:23 -07:00
SpudGunMan e533e1472e fix 2024-08-07 20:06:19 -07:00
SpudGunMan ab00cb11bb Update system.py 2024-08-07 20:05:27 -07:00
SpudGunMan 932b98a634 debug 2024-08-07 20:04:01 -07:00
SpudGunMan b084b0f79e Update mesh_bot.py 2024-08-07 19:59:34 -07:00
Kelly 115d479020 Merge branch 'main' into worldwx 2024-08-07 19:53:37 -07:00
SpudGunMan 1cb9a60bba Update system.py 2024-08-07 19:42:48 -07:00
SpudGunMan 14c304ca2d Update system.py 2024-08-07 19:40:41 -07:00
SpudGunMan 88d1ecc7ec Update system.py 2024-08-07 19:40:20 -07:00
SpudGunMan 7cabff0bc4 Update wx_meteo.py 2024-08-07 19:27:47 -07:00
SpudGunMan 5e0ab39301 requirements update 2024-08-07 19:27:29 -07:00
SpudGunMan f6ff4e2d7d short-strings 2024-08-07 19:20:24 -07:00
SpudGunMan 49c0f3b1c5 Update wx_meteo.py 2024-08-07 19:10:03 -07:00
SpudGunMan fbd38aa147 Update README.md
addressing issue https://github.com/SpudGunMan/meshing-around/issues/23
2024-08-07 18:52:42 -07:00
SpudGunMan 922956e981 Update wx_meteo.py 2024-08-07 18:12:58 -07:00
SpudGunMan ba1447d5f4 inital 2024-08-07 18:04:22 -07:00
15 changed files with 815 additions and 263 deletions
+44 -12
View File
@@ -1,22 +1,26 @@
# meshing-around # meshing-around
Random Mesh Scripts for Network Testing and BBS Activities for Use with Meshtastic Nodes Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
![alt text](etc/pong-bot.jpg "Example Use") ![alt text](etc/pong-bot.jpg "Example Use")
## mesh_bot.sh ## 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.
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`.
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. Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini The bot can also be used to monitor a radio frequency and let you know when high SNR RF activity is seen. Using Hamlib(rigctld) to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher. Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
- Various solar details for radio propagation Full list of commands for the bot.
- Various solar details for radio propagation (spaceWeather module)
- `sun` and `moon` return info on rise and set local time - `sun` and `moon` return info on rise and set local time
- `solar` gives an idea of the x-ray flux - `solar` gives an idea of the x-ray flux
- `hfcond` returns a table of HF solar conditions - `hfcond` returns a table of HF solar conditions
@@ -24,12 +28,12 @@ Any messages that are over 160 characters are chunked into 160 message bytes to
- `bbshelp` returns the following - `bbshelp` returns the following
- `bbslist` list the messages by ID and subject - `bbslist` list the messages by ID and subject
- `bbsread` read a message example use: `bbsread #1` - `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` - `bbsdelete` delete a message example use: `bbsdelete #4`
- Other functions - Other functions
- `whereami` returns the address of location of sender if known - `whereami` returns the address of location of sender if known
- `tide` returns the local tides, NOAA data source - `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 forcasting.
- `wxa` and `wxalert` return NOAA alerts. Short title or expanded details - `wxa` and `wxalert` return NOAA alerts. Short title or expanded details
- `joke` tells a joke - `joke` tells a joke
- `messages` Replay the last messages heard, like Store and Forward - `messages` Replay the last messages heard, like Store and Forward
@@ -41,14 +45,14 @@ 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. Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
## Hardware ## 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. 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.
- Firmware 2.3.14/15 could also have an issue with connectivity with slower devices.
## Install ## Install
Clone the project with `git clone https://github.com/spudgunman/meshing-around` 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` 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. Copy [config.template](config.template) to `config.ini` and edit for your needs.
- Optionally
Optionally:
- `install.sh` will automate optional venv and requirements installation. - `install.sh` will automate optional venv and requirements installation.
- `launch.sh` will activate and launch the app in the venv if built. - `launch.sh` will activate and launch the app in the venv if built.
@@ -80,6 +84,14 @@ Setting the default channel is the channel that won't be spammed by the bot. It'
respond_by_dm_only = True respond_by_dm_only = True
defaultChannel = 0 defaultChannel = 0
``` ```
The weather forcasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use.
```
[location]
enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
```
Modules can be disabled or enabled. Modules can be disabled or enabled.
``` ```
@@ -90,6 +102,19 @@ enabled = False
DadJokes = False DadJokes = False
StoreForward = False StoreForward = False
``` ```
Sentry Bot detects anyone comeing 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) 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!!! 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!!!
@@ -132,6 +157,13 @@ pip install geopy
pip install maidenhead pip install maidenhead
pip install beautifulsoup4 pip install beautifulsoup4
pip install dadjokes pip install dadjokes
pip install geopy
```
The following is needed for open-meteo use
```
pip install openmeteo_requests
pip install retry_requests
pip install numpy
``` ```
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji` To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
@@ -139,7 +171,7 @@ To enable emoji in the Debian console, install the fonts `sudo apt-get install f
# Recognition # Recognition
I used ideas and snippets from other responder bots and want to call them out! I used ideas and snippets from other responder bots and want to call them out!
- https://github.com/Murturtle/MeshLink - 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 - https://github.com/geoffwhittington/meshtastic-matrix-relay
GitHub user PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas GitHub user PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
+24 -9
View File
@@ -32,16 +32,31 @@ 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 welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
# enable or disable the Joke module # enable or disable the Joke module
DadJokes = True DadJokes = True
# enable or disable the Solar module
spaceWeather = True
# StoreForward Enabled and Limits # StoreForward Enabled and Limits
StoreForward = True StoreForward = True
StoreLimit = 3 StoreLimit = 3
# 24 hour clock # 24 hour clock
zuluTime = True zuluTime = False
# wait time for URL requests # wait time for URL requests
URL_TIMEOUT = 10 urlTimeout = 10
# logging to file of the non Bot messages # logging to file of the non Bot messages
LogMessagesToFile = False LogMessagesToFile = 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] [bbs]
enabled = True enabled = True
# list of banned nodes numbers ex: 2813308004,4258675309 # list of banned nodes numbers ex: 2813308004,4258675309
@@ -54,14 +69,14 @@ bbs_admin_list =
enabled = True enabled = True
lat = 48.50 lat = 48.50
lon = -123.0 lon = -123.0
# weather forecast days, the first two rows are today and tonight # NOAA weather forecast days, the first two rows are today and tonight
DAYS_OF_WEATHER = 4 NOAAforecastDuration = 4
# number of weather alerts to display # number of weather alerts to display
ALERT_COUNT = 2 NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA usefull for non US locations
# solar module UseMeteoWxAPI = False
[solar] # Default to metric units rather than imperial
enabled = True useMetric = False
# repeater module # repeater module
[repeater] [repeater]
+1 -1
View File
@@ -8,7 +8,7 @@ After=network.target
[Service] [Service]
WorkingDirectory=/dir/ 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 # Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs # service shows up immediately in systemd's logs
+1 -1
View File
@@ -8,7 +8,7 @@ After=network.target
[Service] [Service]
WorkingDirectory=/dir/ 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 # Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs # service shows up immediately in systemd's logs
+27 -6
View File
@@ -7,26 +7,47 @@ cd "$(dirname "$0")"
sudo usermod -a -G dialout $USER sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $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 cp config.template config.ini
printf "\nConfig file generated\n"
# set virtual environment and install dependencies # set virtual environment and install dependencies
printf "\nMeshing Around Installer\n" 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)" echo "Do you want to install the bot in a virtual environment? (y/n)"
read venv read venv
if [ $venv == "y" ]; then if [ $venv == "y" ]; then
# set virtual environment # set virtual environment
echo "Creating virtual environment..." if ! python3 -m venv --help &> /dev/null
python3 -m venv venv then
source venv/bin/activate 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 # install dependencies
pip install -U -r requirements.txt pip install -U -r requirements.txt
else else
printf "\nSkipping virtual environment...\n" printf "\nSkipping virtual environment...\n"
# install dependencies # 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 read rpi
if [ $rpi == "y" ]; then if [ $rpi == "y" ]; then
pip install -U -r requirements.txt --break-system-packages pip install -U -r requirements.txt --break-system-packages
@@ -36,7 +57,7 @@ else
fi fi
printf "\n\n" 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 read bot
#set the correct path in the service file #set the correct path in the service file
+2 -2
View File
@@ -14,9 +14,9 @@ fi
# launch the application # launch the application
if [ "$1" == "pong" ]; then if [ "$1" == "pong" ]; then
python pong_bot.py python3 pong_bot.py
elif [ "$1" == "mesh" ]; then elif [ "$1" == "mesh" ]; then
python mesh_bot.py python3 mesh_bot.py
else else
printf "\nPlease provide a bot to launch (pong/mesh)" printf "\nPlease provide a bot to launch (pong/mesh)"
fi fi
+193 -146
View File
@@ -10,154 +10,194 @@ from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID): def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
#Auto response to messages #Auto response to messages
bot_response = "" message_lower = message.lower()
if "ping" in message.lower(): bot_response = "I'm sorry, I'm afraid I can't do that."
#Check if the user added @foo to the message
if "@" in message: command_handler = {
if hop == "Direct": "ping": lambda: handle_ping(message, hop, snr, rssi),
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1] "pong": lambda: "🏓PING!!",
else: "motd": lambda: handle_motd(message),
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1] "bbshelp": bbs_help,
else: "wxalert": lambda: handle_wxalert(message_from_id, deviceID),
if hop == "Direct": "wxa": lambda: handle_wxalert(message_from_id, deviceID),
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" "wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
else: "wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
bot_response = "🏓PONG, " + hop "joke": tell_joke,
elif "pong" in message.lower(): "bbslist": bbs_list_messages,
bot_response = "🏓PING!!" "bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
elif "motd" in message.lower(): "bbsread": lambda: handle_bbsread(message),
#check if the user wants to set the motd by using $ "bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
if "$" in message: "messages": lambda: handle_messages(deviceID, channel_number, msg_history, publicChannel),
motd = message.split("$")[1] "cmd": lambda: help_message,
global MOTD "cmd?": lambda: help_message,
MOTD = motd "sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
bot_response = "MOTD Set to: " + MOTD "hfcond": hf_band_conditions,
else: "solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
bot_response = MOTD "lheard": lambda: handle_lheard(),
elif "messages" in message.lower(): "sitrep": lambda: handle_lheard(),
response = "" "whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
for msgH in msg_history: "tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
# check if the message is from the same interface "moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
if msgH[4] == deviceID: "ack": lambda: handle_ack(hop, snr, rssi),
# check if the message is from the same channel "testing": lambda: handle_testing(hop, snr, rssi),
if msgH[2] == channel_number or msgH[2] == publicChannel: "test": lambda: handle_testing(hop, snr, rssi),
# consider message safe to send }
response += f"\n{msgH[0]}: {msgH[1]}" cmds = [] # list to hold the commands found in the message
for key in command_handler:
if len(response) > 0: if key in message_lower.split(' '):
bot_response = "Message History:" + response cmds.append({'cmd': key, 'index': message_lower.index(key)})
else:
bot_response = "No messages in history" if len(cmds) > 0:
elif "bbshelp" in message.lower(): # sort the commands by index value
bot_response = bbs_help() cmds = sorted(cmds, key=lambda k: k['index'])
elif "cmd" in message.lower() or "cmd?" in message.lower(): logger.debug(f"System: Bot Detected: {cmds}")
bot_response = help_message # run the first command after sorting
elif "sun" in message.lower(): bot_response = command_handler[cmds[0]['cmd']]()
location = get_node_location(message_from_id, deviceID, channel_number)
bot_response = get_sun(str(location[0]),str(location[1]))
elif "hfcond" in message.lower():
bot_response = hf_band_conditions()
elif "solar" in message.lower():
bot_response = drap_xray_conditions() + "\n" + solar_conditions()
elif "lheard" in message.lower() or "sitrep" in message.lower():
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
bot_response += "Ch Use: " + str(chutil1) + "%"
if interface2_enabled:
bot_response += " P2:" + str(chutil2) + "%"
elif "whereami" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
where = where_am_i(str(location[0]),str(location[1]))
bot_response = where
elif "tide" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
tide = get_tide(str(location[0]),str(location[1]))
bot_response = tide
elif "moon" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
moon = get_moon(str(location[0]),str(location[1]))
bot_response = moon
elif "wxalert" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxa" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
weatherAlert = getWeatherAlerts(str(location[0]),str(location[1]))
bot_response = weatherAlert
elif "wxc" in message.lower():
location = get_node_location(message_from_id, deviceID, channel_number)
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, channel_number)
weather = get_weather(str(location[0]),str(location[1]))
bot_response = weather
elif "joke" in message.lower():
bot_response = tell_joke()
elif "bbslist" in message.lower():
bot_response = bbs_list_messages()
elif "bbspost" in message.lower():
# Check if the user added a subject to the message
if "$" in message and not "example:" in message:
subject = message.split("$")[1].split("#")[0]
subject = subject.rstrip()
if "#" in message:
body = message.split("#")[1]
body = body.rstrip()
logger.info(f"System: BBS Post: {subject} Body: {body}")
bot_response = bbs_post_message(subject,body,message_from_id)
elif not "example:" in message:
bot_response = "example: bbspost $subject #message"
# Check if the user added a node number to the message
elif "@" in message and not "example:" in message:
toNode = message.split("@")[1].split("#")[0]
toNode = toNode.rstrip()
if "#" in message:
body = message.split("#")[1]
bot_response = bbs_post_dm(toNode, body, message_from_id)
else:
bot_response = "example: bbspost @nodeNumber #message"
elif not "example:" in message:
bot_response = "example: bbspost $subject #message, or bbspost @nodeNumber #message"
elif "bbsread" in message.lower():
# Check if the user added a message number to the message
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_read_message(messageID)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsread #14"
elif "bbsdelete" in message.lower():
# Check if the user added a message number to the message
if "#" in message and not "example:" in message:
messageID = int(message.split("#")[1])
bot_response = bbs_delete_message(messageID, message_from_id)
elif not "example:" in message:
bot_response = "Please add a message number example: bbsdelete #14"
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack # wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7) time.sleep(0.7)
return bot_response 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):
if use_meteo_wxApi:
return "wxalert is not supported"
else:
location = get_node_location(message_from_id, deviceID)
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
return weatherAlert
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]
else:
logger.debug(f"System: bbspost, name lookup found: {toNode}")
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 onReceive(packet, interface): def onReceive(packet, interface):
# extract interface defailts from interface object # extract interface defailts from interface object
rxType = type(interface).__name__ rxType = type(interface).__name__
@@ -263,7 +303,7 @@ def onReceive(packet, interface):
# respond with welcome message on DM # 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)}") 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) 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}") msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else: else:
# message is on a channel # message is on a channel
if messageTrap(message_string): if messageTrap(message_string):
@@ -312,12 +352,12 @@ def onReceive(packet, interface):
elif rxNode == 2: elif rxNode == 2:
logger.debug(f"Repeating message on Device1 Channel:{channel_number}") logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
send_message(rMsg, channel_number, 0, 1) send_message(rMsg, channel_number, 0, 1)
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}") msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
else: else:
# nothing to do for us # nothing to do for us
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\ 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)}") f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}") msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
except KeyError as e: except KeyError as e:
logger.critical(f"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(packet) # print the packet for debugging
@@ -339,9 +379,16 @@ async def start_rx():
if solar_conditions_enabled: if solar_conditions_enabled:
logger.debug(f"System: Celestial Telemetry Enabled") logger.debug(f"System: Celestial Telemetry Enabled")
if location_enabled: if location_enabled:
logger.debug(f"System: Location Telemetry 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: if dad_jokes_enabled:
logger.debug(f"System: Dad Jokes Enabled!") logger.debug(f"System: Dad Jokes 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: if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse: if useDMForResponse:
+20 -2
View File
@@ -7,7 +7,7 @@ import maidenhead as mh # pip install maidenhead
import requests # pip install requests import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4 import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom import xml.dom.minidom
from modules.settings import * from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert") trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
@@ -16,6 +16,7 @@ def where_am_i(lat=0, lon=0):
grid = mh.to_maiden(float(lat), float(lon)) grid = mh.to_maiden(float(lat), float(lon))
if float(lat) == 0 and float(lon) == 0: if float(lat) == 0 and float(lon) == 0:
logger.error("Location: No GPS data, cant find where you are")
return NO_DATA_NOGPS return NO_DATA_NOGPS
# initialize Nominatim API # initialize Nominatim API
@@ -41,6 +42,7 @@ def where_am_i(lat=0, lon=0):
def get_tide(lat=0, lon=0): def get_tide(lat=0, lon=0):
station_id = "" station_id = ""
if float(lat) == 0 and float(lon) == 0: if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, cant find where you are for tide")
return NO_DATA_NOGPS 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" station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
try: try:
@@ -48,14 +50,17 @@ def get_tide(lat=0, lon=0):
if station_data.ok: if station_data.ok:
station_json = station_data.json() station_json = station_data.json()
else: else:
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
if station_json['stationList'] == [] or station_json['stationList'] is None: if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
station_id = station_json['stationList'][0]['stationId'] station_id = station_json['stationList'][0]['stationId']
except (requests.exceptions.RequestException, json.JSONDecodeError): except (requests.exceptions.RequestException, json.JSONDecodeError):
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
@@ -65,8 +70,10 @@ def get_tide(lat=0, lon=0):
try: try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds) station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok: if not station_data.ok:
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException): except (requests.exceptions.RequestException):
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
# extract table class="table table-condensed" # 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: if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS 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) weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
if unit == 1: if unit == 1:
weather_url += "&unit=1" weather_url += "&unit=1"
@@ -105,14 +115,17 @@ def get_weather(lat=0, lon=0, unit=0):
try: try:
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds) weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
if not weather_data.ok: if not weather_data.ok:
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException): except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather data from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
soup = bs.BeautifulSoup(weather_data.text, 'html.parser') soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
table = soup.find('div', id="detailed-forecast-body") table = soup.find('div', id="detailed-forecast-body")
if table is None: if table is None:
logger.error("Location:Bad weather data from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
else: else:
# get rows # get rows
@@ -200,8 +213,10 @@ def getWeatherAlerts(lat=0, lon=0):
try: try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds) alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok: if not alert_data.ok:
logger.error("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException): except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
alerts = "" alerts = ""
@@ -233,6 +248,7 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA # get the latest details of weather alerts from NOAA
alerts = "" alerts = ""
if float(lat) == 0 and float(lon) == 0: if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, cant find where you are for weather alerts")
return NO_DATA_NOGPS 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?point=" + str(lat) + "," + str(lon)
@@ -241,8 +257,10 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
try: try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds) alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok: if not alert_data.ok:
logger.error("Location:Error fetching weather alerts detailed from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException): except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather alerts detailed from NOAA")
return ERROR_FETCHING_DATA return ERROR_FETCHING_DATA
alerts = "" alerts = ""
+4 -3
View File
@@ -5,7 +5,7 @@
import socket import socket
import asyncio import asyncio
from modules.settings import * from modules.log import *
def get_hamlib(msg="f"): def get_hamlib(msg="f"):
try: try:
@@ -13,7 +13,7 @@ def get_hamlib(msg="f"):
rigControlSocket.settimeout(2) rigControlSocket.settimeout(2)
rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1]))) rigControlSocket.connect((rigControlServerAddress.split(":")[0],int(rigControlServerAddress.split(":")[1])))
except Exception as e: 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 return ERROR_FETCHING_DATA
try: try:
@@ -27,7 +27,7 @@ def get_hamlib(msg="f"):
data = data.replace(b'\n',b'') data = data.replace(b'\n',b'')
return data.decode("utf-8").rstrip() return data.decode("utf-8").rstrip()
except Exception as e: 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 return ERROR_FETCHING_DATA
def get_freq_common_name(freq): def get_freq_common_name(freq):
@@ -140,6 +140,7 @@ async def signalWatcher():
signalStrength = int(get_sig_strength()) signalStrength = int(get_sig_strength())
if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold: if signalStrength >= previousStrength and signalStrength > signalDetectionThreshold:
message = f"Detected {get_freq_common_name(get_hamlib('f'))} active. S-Meter:{signalStrength}dBm" 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 previousStrength = signalStrength
signalCycle = 0 signalCycle = 0
await asyncio.sleep(signalHoldTime) await asyncio.sleep(signalHoldTime)
+47 -13
View File
@@ -40,6 +40,26 @@ if config.sections() == []:
config.write(open(config_file, 'w')) config.write(open(config_file, 'w'))
print (f"System: Config file created, check {config_file} or review the config.template") 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 settings
interface1_type = config['interface'].get('type', 'serial') interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '') port1 = config['interface'].get('port', '')
@@ -58,29 +78,42 @@ else:
# variables # variables
try: try:
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True) useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
location_enabled = config['location'].getboolean('enabled', False) zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
latitudeValue = config['location'].getfloat('lat', 48.50) log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
longitudeValue = config['location'].getfloat('lon', -123.0) urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
zuluTime = config['general'].getboolean('zuluTime', False) store_forward_enabled = config['general'].getboolean('StoreForward', True) # default False
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG) 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 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', True)
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
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) bbs_enabled = config['bbs'].getboolean('enabled', False)
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl') bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
store_forward_enabled = config['general'].getboolean('StoreForward', False)
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
config['general'].get('motd', MOTD)
urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds
forecastDuration = config['general'].getint('DAYS_OF_WEATHER', 4) # default days of weather
numWxAlerts = config['general'].getint('ALERT_COUNT', 2) # default 2 alerts
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',') bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',') bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
repeater_enabled = config['repeater'].getboolean('enabled', False) repeater_enabled = config['repeater'].getboolean('enabled', False)
repeater_channels = config['repeater'].get('repeater_channels', '').split(',') repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
radio_dectection_enabled = config['radioMon'].getboolean('enabled', False) radio_dectection_enabled = config['radioMon'].getboolean('enabled', False)
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532 rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
sigWatchBrodcastCh = config['radioMon'].get('sigWatchBrodcastCh', '2').split(',') # default Channel 2 sigWatchBrodcastCh = config['radioMon'].get('sigWatchBrodcastCh', '2').split(',') # default Channel 2
@@ -88,6 +121,7 @@ try:
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
except KeyError as e: except KeyError as e:
print(f"System: Error reading config file: {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: Check the config.ini against config.template file for missing sections or values.")
+9 -5
View File
@@ -7,7 +7,7 @@ import xml.dom.minidom
from datetime import datetime from datetime import datetime
import ephem # pip install pyephem import ephem # pip install pyephem
from datetime import timedelta from datetime import timedelta
from modules.settings import * from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond") trap_list_solarconditions = ("sun", "solar", "hfcond")
@@ -19,9 +19,11 @@ def hf_band_conditions():
solarxml = xml.dom.minidom.parseString(band_cond.text) solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"): for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n" 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: else:
hf_cond += ERROR_FETCHING_DATA logger.error("Solar: Error fetching HF band conditions")
hf_cond = hf_cond[:-1] # remove the last newline hf_cond = ERROR_FETCHING_DATA
return hf_cond return hf_cond
def solar_conditions(): def solar_conditions():
@@ -39,7 +41,8 @@ def solar_conditions():
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data 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 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: else:
solar_cond += ERROR_FETCHING_DATA logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond return solar_cond
def drap_xray_conditions(): def drap_xray_conditions():
@@ -53,7 +56,8 @@ def drap_xray_conditions():
if x_filter in line: if x_filter in line:
xray_flux = line.split(": ")[1] xray_flux = line.split(": ")[1]
else: else:
xray_flux += ERROR_FETCHING_DATA logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux return xray_flux
def get_sun(lat=0, lon=0): def get_sun(lat=0, lon=0):
+191 -18
View File
@@ -20,22 +20,36 @@ if ping_enabled:
trap_list = trap_list + trap_list_ping trap_list = trap_list + trap_list_ping
help_message = help_message + "ping" help_message = help_message + "ping"
# Sitrep Configuration
if sitrep_enabled: if sitrep_enabled:
trap_list_sitrep = ("sitrep", "lheard") trap_list_sitrep = ("sitrep", "lheard")
trap_list = trap_list + trap_list_sitrep trap_list = trap_list + trap_list_sitrep
help_message = help_message + ", 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 # Solar Conditions Configuration
if solar_conditions_enabled: if solar_conditions_enabled:
from modules.solarconditions import * # from the spudgunman/meshing-around repo from modules.solarconditions import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon 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 # Location Configuration
if location_enabled: if location_enabled:
from modules.locationdata import * # from the spudgunman/meshing-around repo from modules.locationdata import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx 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 # BBS Configuration
if bbs_enabled: if bbs_enabled:
@@ -49,6 +63,10 @@ if dad_jokes_enabled:
trap_list = trap_list + ("joke",) trap_list = trap_list + ("joke",)
help_message = help_message + ", joke" help_message = help_message + ", joke"
if sentry_enabled:
from math import sqrt
import geopy.distance # pip install geopy
# Store and Forward Configuration # Store and Forward Configuration
if store_forward_enabled: if store_forward_enabled:
trap_list = trap_list + ("messages",) trap_list = trap_list + ("messages",)
@@ -142,6 +160,32 @@ 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 name = str(decimal_to_hex(number)) # If name not found, use the ID as string
return name return name
return number 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():
if 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 str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
if nodeInt == 2:
for node in interface2.nodes.values():
if str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
else:
# try other interface
if interface2_enabled:
for node in interface1.nodes.values():
if str(short_name.lower()) == node['user']['shortName'].lower():
return node['num']
return 0
def get_node_list(nodeInt=1): def get_node_list(nodeInt=1):
# Get a list of nodes on the device # Get a list of nodes on the device
@@ -175,8 +219,10 @@ def get_node_list(nodeInt=1):
node_name = get_name_from_number(node['num'], 'long', nodeInt) node_name = get_name_from_number(node['num'], 'long', nodeInt)
snr = node.get('snr', 0) snr = node.get('snr', 0)
# issue where lastHeard is not always present # issue where lastHeard is not always present, also had issues with None
last_heard = node.get('lastHeard', 0) last_heard = node.get('lastHeard', 0)
if last_heard is None:
last_heard = 0
# make a list of nodes with last heard time and SNR # make a list of nodes with last heard time and SNR
item = (node_name, last_heard, snr) item = (node_name, last_heard, snr)
@@ -192,18 +238,23 @@ def get_node_list(nodeInt=1):
node_list2.sort(key=lambda x: x[1], reverse=True) node_list2.sort(key=lambda x: x[1], reverse=True)
except Exception as e: except Exception as e:
logger.error(f"System: Error sorting node list: {e}") logger.error(f"System: Error sorting node list: {e}")
print (f"Node List1: {node_list1[:5]}\n") #print (f"Node List1: {node_list1[:5]}\n")
print (f"Node List2: {node_list2[:5]}\n") #print (f"Node List2: {node_list2[:5]}\n")
node_list = ERROR_FETCHING_DATA
# make a nice list for the user try:
for x in node_list1[:SITREP_NODE_COUNT]: # make a nice list for the user
short_node_list.append(f"{x[0]} SNR:{x[2]}") for x in node_list1[:SITREP_NODE_COUNT]:
for x in node_list2[:SITREP_NODE_COUNT]: short_node_list.append(f"{x[0]} SNR:{x[2]}")
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: for x in short_node_list:
if x != "" or x != '\n': if x != "" or x != '\n':
node_list += 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 return node_list
@@ -261,6 +312,72 @@ def get_node_location(number, nodeInt=1, channel=0):
else: else:
logger.warning(f"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']
# set radius around BOT position
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
# calculate distance to node and report
except Exception as e:
pass
# 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']
# set radius around BOT position
distance = geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m
if (distance < sentry_radius):
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
# calculate distance to node and report
except Exception as e:
pass
#sort by distance closest to lattitudeValue, longitudeValue
node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
# 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): def send_message(message, ch, nodeid=0, nodeInt=1):
if message == "": if message == "":
@@ -293,14 +410,14 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
for m in message_list: for m in message_list:
if nodeid == 0: if nodeid == 0:
#Send to channel #Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + f"{m}") logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending Multi-Chunk Message: " + CustomFormatter.white + m.replace('\n', ' '))
if nodeInt == 1: if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch) interface1.sendText(text=m, channelIndex=ch)
if nodeInt == 2: if nodeInt == 2:
interface2.sendText(text=m, channelIndex=ch) interface2.sendText(text=m, channelIndex=ch)
else: else:
# Send to DM # Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + f"{m}" + CustomFormatter.purple +\ logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending Multi-Chunk DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}") " To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1: if nodeInt == 1:
interface1.sendText(text=m, channelIndex=ch, destinationId=nodeid) interface1.sendText(text=m, channelIndex=ch, destinationId=nodeid)
@@ -309,14 +426,14 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
else: # message is less than MESSAGE_CHUNK_SIZE characters else: # message is less than MESSAGE_CHUNK_SIZE characters
if nodeid == 0: if nodeid == 0:
# Send to channel # Send to channel
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + f"{message}") logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "Sending: " + CustomFormatter.white + message.replace('\n', ' '))
if nodeInt == 1: if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch) interface1.sendText(text=message, channelIndex=ch)
if nodeInt == 2: if nodeInt == 2:
interface2.sendText(text=message, channelIndex=ch) interface2.sendText(text=message, channelIndex=ch)
else: else:
# Send to DM # Send to DM
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + f"{message}" + CustomFormatter.purple +\ logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}") " To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
if nodeInt == 1: if nodeInt == 1:
interface1.sendText(text=message, channelIndex=ch, destinationId=nodeid) interface1.sendText(text=message, channelIndex=ch, destinationId=nodeid)
@@ -481,6 +598,13 @@ def suppress_stdout():
async def watchdog(): async def watchdog():
global retry_int1, retry_int2 global retry_int1, retry_int2
if sentry_enabled:
sentry_loop = 0
lastSpotted = ""
enemySpotted = ""
sentry_loop2 = 0
lastSpotted2 = ""
enemySpotted2 = ""
# watchdog for connection to the interface # watchdog for connection to the interface
while True: while True:
await asyncio.sleep(20) await asyncio.sleep(20)
@@ -494,6 +618,31 @@ async def watchdog():
logger.error(f"System: communicating with interface1, trying to reconnect: {e}") logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
retry_int1 = True 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: if retry_int1:
try: try:
await retry_interface(1) await retry_interface(1)
@@ -508,10 +657,34 @@ async def watchdog():
except Exception as e: except Exception as e:
logger.error(f"System: communicating with interface2, trying to reconnect: {e}") logger.error(f"System: communicating with interface2, trying to reconnect: {e}")
retry_int2 = True 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'])
enemySpotted += f" at {closest_nodes1[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: if retry_int2:
try: try:
await retry_interface(2) await retry_interface(2)
except Exception as e: except Exception as e:
logger.error(f"System: retrying interface2: {e}") logger.error(f"System: retrying interface2: {e}")
+177
View File
@@ -0,0 +1,177 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#import requests_cache
from modules.log import *
def get_wx_meteo(lat=0, lon=0, unit=0):
# set forcast days 1 or 3
forecastDays = 3
# Setup the Open-Meteo API client with cache and retry on error
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
retry_session = retry(retries = 3, backoff_factor = 0.2)
openmeteo = openmeteo_requests.Client(session = retry_session)
# Make sure all required weather variables are listed here
# The order of variables in hourly or daily is important to assign them correctly below
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": {lat},
"longitude": {lon},
"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_hours", "precipitation_probability_max", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant"],
"timezone": "auto",
"forecast_days": {forecastDays}
}
# Unit 0 is imperial, 1 is metric
if unit == 0:
params["temperature_unit"] = "fahrenheit"
params["wind_speed_unit"] = "mph"
params["precipitation_unit"] = "inch"
params["distance_unit"] = "mile"
params["pressure_unit"] = "inHg"
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
except Exception as e:
logger.error(f"Error fetching meteo weather data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
response = responses[0]
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
# Process daily data. The order of variables needs to be the same as requested.
daily = response.Daily()
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
# convert wind value to cardinal directions
for value in daily_wind_direction_10m_dominant:
if value < 22.5:
wind_direction = "N"
elif value < 67.5:
wind_direction = "NE"
elif value < 112.5:
wind_direction = "E"
elif value < 157.5:
wind_direction = "SE"
elif value < 202.5:
wind_direction = "S"
elif value < 247.5:
wind_direction = "SW"
elif value < 292.5:
wind_direction = "W"
elif value < 337.5:
wind_direction = "NW"
else:
wind_direction = "N"
# create a weather report
weather_report = ""
for i in range(forecastDays):
if str(i + 1) == "1":
weather_report += "Today, "
elif str(i + 1) == "2":
weather_report += "Tomorrow, "
else:
weather_report += "Futurecast: "
# report weather from WMO Weather interpretation codes (WW)
code_string = ""
if daily_weather_code[i] == 0:
code_string = "Clear sky"
elif daily_weather_code[i] == 1 or 2 or 3:
code_string = "Partly cloudy"
elif daily_weather_code[i] == 45 or 48:
code_string = "Fog"
elif daily_weather_code[i] == 51:
code_string = "Drizzle: Light"
elif daily_weather_code[i] == 53:
code_string = "Drizzle: Moderate"
elif daily_weather_code[i] == 55:
code_string = "Drizzle: Heavy"
elif daily_weather_code[i] == 56:
code_string = "Freezing Drizzle: Light"
elif daily_weather_code[i] == 57:
code_string = "Freezing Drizzle: Moderate"
elif daily_weather_code[i] == 61:
code_string = "Rain: Slight"
elif daily_weather_code[i] == 63:
code_string = "Rain: Moderate"
elif daily_weather_code[i] == 65:
code_string = "Rain: Heavy"
elif daily_weather_code[i] == 66:
code_string = "Freezing Rain: Light"
elif daily_weather_code[i] == 67:
code_string = "Freezing Rain: Dense"
elif daily_weather_code[i] == 71:
code_string = "Snow: Light"
elif daily_weather_code[i] == 73:
code_string = "Snow: Moderate"
elif daily_weather_code[i] == 75:
code_string = "Snow: Heavy"
elif daily_weather_code[i] == 77:
code_string = "Snow Grains"
elif daily_weather_code[i] == 80:
code_string = "Rain showers: Slight"
elif daily_weather_code[i] == 81:
code_string = "Rain showers: Moderate"
elif daily_weather_code[i] == 82:
code_string = "Rain showers: Heavy"
elif daily_weather_code[i] == 85:
code_string = "Snow showers: Light"
elif daily_weather_code[i] == 86:
code_string = "Snow showers: Moderate"
elif daily_weather_code[i] == 95:
code_string = "Thunderstorm: Slight"
elif daily_weather_code[i] == 96:
code_string = "Thunderstorm: Moderate"
elif daily_weather_code[i] == 99:
code_string = "Thunderstorm: Heavy"
weather_report += "Cond: " + code_string + ". "
# report temperature
if unit == 0:
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "F, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "F. "
else:
weather_report += "High: " + str(int(round(daily_temperature_2m_max[i]))) + "C, with a low of " + str(int(round(daily_temperature_2m_min[i]))) + "C. "
# check for precipitation
if daily_precipitation_hours[i] > 0:
if unit == 0:
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "in, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
else:
weather_report += "Precip: " + str(round(daily_precipitation_probability_max[i],2)) + "mm, in " + str(round(daily_precipitation_hours[i],2)) + " hours. "
else:
weather_report += "No Precip. "
# check for wind
if daily_wind_speed_10m_max[i] > 0:
if unit == 0:
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "mph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "mph from:" + wind_direction + "."
else:
weather_report += "Wind: " + str(int(round(daily_wind_speed_10m_max[i]))) + "kph, gusts up to " + str(int(round(daily_wind_gusts_10m_max[i]))) + "kph from:" + wind_direction + "."
else:
weather_report += "No Wind\n"
# add a new line for the next day
if i < forecastDays - 1:
weather_report += "\n"
return weather_report
+71 -45
View File
@@ -10,57 +10,81 @@ from modules.system import *
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID): def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
# Auto response to messages # Auto response to messages
if "ping" in message.lower(): message_lower = message.lower()
# Check if the user added @foo to the message bot_response = "I'm sorry, I'm afraid I can't do that."
if "@" in message:
if hop == "Direct": command_handler = {
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1] "ping": lambda: handle_ping(message, hop, snr, rssi),
else: "pong": lambda: "🏓Ping!!",
bot_response = "🏓PONG, " + hop + " and copy: " + message.split("@")[1] "motd": lambda: handle_motd(message, MOTD),
else: "cmd": lambda: help_message,
if hop == "Direct": "cmd?": lambda: help_message,
bot_response = "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" "lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
else: "sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
bot_response = "🏓PONG, " + hop "ack": lambda: handle_ack(hop, snr, rssi),
elif "pong" in message.lower(): "testing": lambda: handle_testing(hop, snr, rssi),
bot_response = "🏓Ping!!" "test": lambda: handle_testing(hop, snr, rssi),
elif "motd" in message.lower(): }
# check if the user wants to set the motd by using $ cmds = [] # list to hold the commands found in the message
if "$" in message: for key in command_handler:
motd = message.split("$")[1] if key in message_lower.split(' '):
global MOTD cmds.append({'cmd': key, 'index': message_lower.index(key)})
MOTD = motd
bot_response = "MOTD Set to: " + MOTD if len(cmds) > 0:
else: # sort the commands by index value
bot_response = MOTD cmds = sorted(cmds, key=lambda k: k['index'])
elif "cmd" in message.lower() or "cmd?" in message.lower(): logger.debug(f"System: Bot Detected: {cmds}")
bot_response = help_message # run the first command after sorting
elif "lheard" in message.lower() or "sitrep" in message.lower(): bot_response = command_handler[cmds[0]['cmd']]()
bot_response = "Last heard:\n" + str(get_node_list(1))
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil1 = "{:.2f}".format(chutil1)
if interface2_enabled:
bot_response += "Port2:\n" + str(get_node_list(2))
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
chutil2 = "{:.2f}".format(chutil2)
elif "ack" in message.lower():
if hop == "Direct":
bot_response = "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓ACK-ACK! " + hop
elif "testing" in message.lower() or "test" in message.lower():
if hop == "Direct":
bot_response = "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
else:
bot_response = "🏓Testing 1,2,3 " + hop
else:
bot_response = "I'm sorry, I'm afraid I can't do that."
# wait a 700ms to avoid message collision from lora-ack # wait a 700ms to avoid message collision from lora-ack
time.sleep(0.7) time.sleep(0.7)
return bot_response 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 onReceive(packet, interface): def onReceive(packet, interface):
# extract interface defailts from interface object # extract interface defailts from interface object
rxType = type(interface).__name__ rxType = type(interface).__name__
@@ -221,6 +245,8 @@ async def start_rx():
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}") f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
if log_messages_to_file: if log_messages_to_file:
logger.debug(f"System: Logging Messages to disk") logger.debug(f"System: Logging Messages to disk")
if sentry_enabled:
logger.debug(f"System: Sentry Enabled")
if store_forward_enabled: if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse: if useDMForResponse:
+4
View File
@@ -7,3 +7,7 @@ geopy
maidenhead maidenhead
beautifulsoup4 beautifulsoup4
dadjokes dadjokes
openmeteo_requests
retry_requests
numpy
geopy