Compare commits

...

67 Commits

Author SHA1 Message Date
Kelly
b8d33cc270 Merge pull request #97 from SpudGunMan/emergencyalert
EnhanceEmergencyAlert
2024-12-20 14:39:31 -08:00
SpudGunMan
a6ce9e9211 remove Numpy 2024-12-20 12:22:01 -08:00
SpudGunMan
60bdabdd1b embedded 2024-12-20 02:00:36 -08:00
SpudGunMan
9c5c2080cf Update locationdata_eu.py 2024-12-20 01:17:47 -08:00
SpudGunMan
8f758229cb Update install.sh 2024-12-20 00:38:59 -08:00
SpudGunMan
8ac9c53f1a enhance groupPing 2024-12-19 18:29:35 -08:00
SpudGunMan
98cbf5528c fixEmbedded 2024-12-19 17:46:26 -08:00
SpudGunMan
6296150677 Update pong_bot.py 2024-12-19 17:40:10 -08:00
SpudGunMan
13cb1e8df9 Update mesh_bot.py 2024-12-19 17:39:15 -08:00
SpudGunMan
e26e876ccf Update system.py 2024-12-19 17:21:33 -08:00
SpudGunMan
550b50f74e Update settings.py 2024-12-19 17:06:24 -08:00
SpudGunMan
ac5aa1a201 Update system.py 2024-12-19 17:03:53 -08:00
SpudGunMan
19700f54c5 Update system.py 2024-12-19 16:55:26 -08:00
SpudGunMan
7e5626cd30 Update system.py 2024-12-19 16:27:09 -08:00
SpudGunMan
c27b6ed8a1 enhanceEmergency Alerting 2024-12-19 16:18:38 -08:00
SpudGunMan
717181bcd0 Update locationdata_eu.py 2024-12-19 16:07:07 -08:00
SpudGunMan
4d5916df29 Update settings.py 2024-12-18 19:58:34 -08:00
SpudGunMan
93b7a1d613 enableGBalerts 2024-12-18 19:58:21 -08:00
SpudGunMan
35cc029984 Update README.md 2024-12-18 19:54:47 -08:00
SpudGunMan
589d44c152 Update locationdata_eu.py 2024-12-18 19:52:49 -08:00
SpudGunMan
06a14d875f enableUKalerts 2024-12-18 19:39:55 -08:00
SpudGunMan
454f823ad7 england.GovAlert 2024-12-18 19:33:11 -08:00
SpudGunMan
6974c4ef66 Update locationdata_eu.py 2024-12-18 19:24:33 -08:00
SpudGunMan
bd956dfebc locationEnhance 2024-12-18 15:09:38 -08:00
SpudGunMan
4aaac5ba49 Update README.md 2024-12-18 13:57:58 -08:00
SpudGunMan
2ae792dd8d Update README.md 2024-12-18 10:55:53 -08:00
SpudGunMan
ca033f024e enhanceNews
returns a random line from the file
2024-12-18 10:53:44 -08:00
SpudGunMan
ad11f787de Update locationdata_eu.py 2024-12-17 23:12:52 -08:00
SpudGunMan
e3d1607c86 enhance EU 2024-12-17 23:11:05 -08:00
SpudGunMan
b68461cbc8 move the moon 2024-12-17 22:57:14 -08:00
SpudGunMan
ddad35aa1e Update README.md 2024-12-17 22:42:15 -08:00
SpudGunMan
35f4aad6f8 riverFlow 2024-12-17 22:16:02 -08:00
SpudGunMan
f08f98e040 Update locationdata.py 2024-12-17 21:55:12 -08:00
SpudGunMan
467376d9c7 Update mesh_bot.py 2024-12-17 21:47:05 -08:00
SpudGunMan
1cbdc93632 riverFlowAlpha 2024-12-17 20:32:07 -08:00
SpudGunMan
2323015617 riverFlood 2024-12-17 20:06:16 -08:00
SpudGunMan
51de0dee8a riverFlow 2024-12-17 13:32:08 -08:00
SpudGunMan
b74c0ebd36 Update wx_meteo.py 2024-12-17 13:29:15 -08:00
SpudGunMan
0a4c54a5a2 Update locationdata.py 2024-12-17 12:14:00 -08:00
SpudGunMan
481809493c Update wx_meteo.py 2024-12-16 20:56:28 -08:00
SpudGunMan
c3914e0423 Update mesh_bot.py 2024-12-16 20:54:57 -08:00
SpudGunMan
ac40254bc4 refactor Openmeteo wx
eliminate requirement for modules and use requests native
2024-12-16 20:43:52 -08:00
SpudGunMan
b6540a1d20 🚨improve EAS duplicates 2024-12-16 09:05:59 -08:00
Kelly
87d29d123f Merge pull request #96 from todd2982/patch-1 2024-12-16 08:05:24 -08:00
todd2982
0aa6f8cc07 Patch CVE found in base python image
Patches the following CVE:
CVE-2024-6345
CVE-2023-5752
2024-12-16 08:21:01 -06:00
SpudGunMan
e2bb480f5f output fix femtofox
Python 3.10.12 had issues
2024-12-15 01:04:34 -08:00
SpudGunMan
920f951e47 Update Dockerfile 2024-12-14 23:00:18 -08:00
SpudGunMan
215fe76f2a CodeQLBadge 2024-12-13 23:27:14 -08:00
SpudGunMan
1740bbf666 Update install.sh 2024-12-13 21:35:10 -08:00
SpudGunMan
f9370d47b4 Update install.sh 2024-12-13 21:34:01 -08:00
SpudGunMan
91072cb47d Update install.sh 2024-12-13 21:29:09 -08:00
SpudGunMan
c30be37f02 femtofox 2024-12-13 21:27:35 -08:00
SpudGunMan
d51dadba04 Update install.sh 2024-12-13 21:20:57 -08:00
SpudGunMan
99c404f479 moveThisShakeThat 2024-12-13 20:12:40 -08:00
SpudGunMan
659ee2959c cleanup 2024-12-13 20:10:59 -08:00
SpudGunMan
1ac9f3b0d6 loop detector 2024-12-13 20:04:20 -08:00
SpudGunMan
d0dc737863 Update README.md 2024-12-13 14:57:14 -08:00
SpudGunMan
e438c82a11 enhance 2024-12-13 14:19:22 -08:00
SpudGunMan
9d7d4601dc Update system.py 2024-12-13 13:29:10 -08:00
SpudGunMan
fdd741446c Update system.py 2024-12-13 13:15:11 -08:00
SpudGunMan
fdbab1685f Update locationdata.py 2024-12-13 13:06:05 -08:00
SpudGunMan
ed0940b126 🧀 2024-12-13 13:03:41 -08:00
SpudGunMan
a087c7bb3a Update system.py 2024-12-13 13:02:06 -08:00
SpudGunMan
0439db2ec0 sysinfo
returns telemetry info
2024-12-13 12:59:12 -08:00
SpudGunMan
c1a5d4d336 Create gpio.py 2024-12-13 12:30:49 -08:00
SpudGunMan
eeffc6361a enhance@🏓 2024-12-13 11:45:50 -08:00
SpudGunMan
e2be3c20b7 enhance🏓 2024-12-13 10:30:18 -08:00
15 changed files with 711 additions and 322 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.10-slim
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/l
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV TZ="America/Los_Angeles"
WORKDIR /app

View File

@@ -5,6 +5,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
![Example Use](etc/pong-bot.jpg "Example Use")
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
### Intelligent Keyword Responder
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
@@ -28,7 +29,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
- **Wiki Integration**: Look up data using Wikipedia results.
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
- **Satalite Pass Info**: Get passes for satalite at your location.
@@ -48,6 +49,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
- **UK.GOV Alerts**: Pulling data form the UK.GOV alert page
### File Monitor Alerts
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
@@ -60,7 +62,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
## Getting Started
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, or [femtofox](https://github.com/noon92/femtofox) project for embedding, possibly see the [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), there is also [femtofox](https://github.com/noon92/femtofox). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
### Installation
@@ -145,6 +147,7 @@ enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
```
### Module Settings
@@ -210,19 +213,17 @@ alert_interface = 1
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS
This uses the SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. femaAlertBroadcastCh is currently not written, still under development.
#### FEMA iPAWS/EAS and UK.gov
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. UK.gov for England
```ini
# FEMA IPAWS/CAP Alert Broadcast
femaAlertBroadcastEnabled = True
# FEMA IPAWS/CAP Alert Broadcast Channels
femaAlertBroadcastCh = 2,4
# Ignore any headline that includes the word Test
ignoreFEMAtest = True
# comma separated list of codes trigger local alert. (e.g., SAME, FIPS, ZIP)
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
ignoreFEMAtest = True # Ignore any headline that includes the word Test
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
enableGBalerts = False # use UK.gov for alert source
```
#### NOAA EAS
@@ -287,6 +288,7 @@ file_path = alert.txt
broadcastCh = 2,4
enable_read_news = False
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
```
#### Offline EAS
@@ -358,6 +360,7 @@ There is no direct support for MQTT in the code, however, reports from Discord a
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
| `history` | Returns the last commands run by user(s) | ✅ |
| `sysinfo` | Returns the bot node telemetry info | ✅ |
| `cmd` | Returns the list of commands (the help message) | ✅ |
### Radio Propagation & Weather Forcasting
@@ -370,7 +373,8 @@ There is no direct support for MQTT in the code, however, reports from Discord a
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `wx` and `wxc` | Return 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 | |
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts. Headline or expanded details | |
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
### Bulletin Board & Mail
| Command | Description | |
@@ -459,14 +463,6 @@ pip install schedule
pip install wikipedia
```
For open-meteo use:
```sh
pip install openmeteo_requests
pip install retry_requests
pip install numpy
```
For the Ollama LLM:
```sh
@@ -478,3 +474,5 @@ To enable emoji in the Debian console, install the fonts:
```sh
sudo apt-get install fonts-noto-color-emoji
```
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.

View File

@@ -25,7 +25,7 @@ port = /dev/ttyUSB0
[general]
# if False will respond on all channels but the default channel
respond_by_dm_only = True
# Allows auto-ping feature in a channel, False forces DM
# Allows auto-ping feature in a channel, False forces to 1 ping only
autoPingInChannel = False
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
defaultChannel = 0
@@ -108,33 +108,6 @@ bbslink_enabled = False
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
bbslink_whitelist =
[smtp]
# enable or disable the SMTP module
enableSMTP = False
# enable or disable the IMAP module for inbound email
enableImap = False
# list of Sysop Emails seperate with commas
sysopEmails =
SMTP_SERVER = smtp.gmail.com
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
SMTP_PORT = 587
# Sender email: be mindful of public access, don't use your personal email
FROM_EMAIL = none@gmail.com
SMTP_AUTH = True
SMTP_USERNAME = none@gmail.com
SMTP_PASSWORD = none
EMAIL_SUBJECT = Meshtastic✉
# IMAP not implimented yet
IMAP_SERVER = imap.gmail.com
# 993 IMAP over TLS/SSL, 143 legacy IMAP
IMAP_PORT = 993
# IMAP login usually same as SMTP
IMAP_USERNAME = none@gmail.com
IMAP_PASSWORD = none
IMAP_FOLDER = inbox
# location module
[location]
enabled = True
@@ -151,23 +124,28 @@ repeaterLookup = rbook
NOAAforecastDuration = 4
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# NOAA Hydrology unique identifiers, LID or USGS ID
riverListDefault =
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# FEMA IPAWS/CAP Alert Broadcast
femaAlertBroadcastEnabled = False
# FEMA IPAWS/CAP Alert Broadcast Channels
femaAlertBroadcastCh = 2
# Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastEnabled = False
# Goverment Emergency IPAWS/CAP Alert Broadcast Channels
eAlertBroadcastCh = 2
# Ignore any headline that includes the word Test
ignoreFEMAtest = True
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
enableGBalerts = False
# Satalite Pass Prediction
# Register for free API https://www.n2yo.com/login/
@@ -208,6 +186,34 @@ file_path = alert.txt
broadcastCh = 2
enable_read_news = False
news_file_path = news.txt
# only return a single random line from the news file
news_random_line = False
[smtp]
# enable or disable the SMTP module
enableSMTP = False
# enable or disable the IMAP module for inbound email
enableImap = False
# list of Sysop Emails seperate with commas
sysopEmails =
SMTP_SERVER = smtp.gmail.com
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
SMTP_PORT = 587
# Sender email: be mindful of public access, don't use your personal email
FROM_EMAIL = none@gmail.com
SMTP_AUTH = True
SMTP_USERNAME = none@gmail.com
SMTP_PASSWORD = none
EMAIL_SUBJECT = Meshtastic✉
# IMAP not implimented yet
IMAP_SERVER = imap.gmail.com
# 993 IMAP over TLS/SSL, 143 legacy IMAP
IMAP_PORT = 993
# IMAP login usually same as SMTP
IMAP_USERNAME = none@gmail.com
IMAP_PASSWORD = none
IMAP_FOLDER = inbox
[games]
# if hop limit for the user exceeds this value, the message will be dropped

View File

@@ -11,30 +11,49 @@ printf "\nThis script will try and install the Meshing Around Bot and its depend
printf "Installer works best in raspian/debian/ubuntu, if there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# Check and install dependencies
if ! command -v python3 &> /dev/null
then
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
sudo apt-get install python3 python3-pip
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, trying 'apt-get install python3-pip'\n"
sudo apt-get install python3-pip
fi
# check if running on embedded
printf "\nAre You installing into an embedded system? (y/n)"
read embedded
# double check for python3 and pip
if ! command -v python3 &> /dev/null
then
printf "python3 not found, please install python3 with your OS\n"
exit 1
if [ $embedded == "y" ]; then
printf "\nDetected embedded skipping dependency installation\n"
if [ $program_path != "/opt/meshing-around" ]; then
printf "\nIt is suggested to project path to /opt/meshing-around\n"
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
read move
if [ $move == "y" ]; then
sudo mv $program_path /opt/meshing-around
cd /opt/meshing-around
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
exit 0
fi
fi
else
# Check and install dependencies
if ! command -v python3 &> /dev/null
then
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
sudo apt-get install python3 python3-pip
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, trying 'apt-get install python3-pip'\n"
sudo apt-get install python3-pip
fi
# double check for python3 and pip
if ! command -v python3 &> /dev/null
then
printf "python3 not found, please install python3 with your OS\n"
exit 1
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
exit 1
fi
printf "\nDependencies installed\n"
fi
if ! command -v pip &> /dev/null
then
printf "pip not found, please install pip with your OS\n"
exit 1
fi
printf "\nDependencies installed\n"
# add user to groups for serial access
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
@@ -56,58 +75,63 @@ fi
cp config.template config.ini
printf "\nConfig files generated!\n"
printf "\nDo you want to install the bot in a python virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3/venv error, please install python3-venv with your OS\n"
exit 1
else
echo "The Following could be messy, or take some time on slower devices."
echo "Creating virtual environment..."
#check if python3 has venv module
if [ -f venv/bin/activate ]; then
printf "\nFound virtual environment for python\n"
python3 -m venv venv
source venv/bin/activate
else
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
sudo apt-get install python3-venv
fi
# create virtual environment
python3 -m venv venv
# double check for python3-venv
if [ -f venv/bin/activate ]; then
printf "\nFound virtual environment for python\n"
source venv/bin/activate
else
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
exit 1
fi
printf "\nVirtual environment created\n"
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies
pip install -U -r requirements.txt
fi
# check if running on embedded
if [ $embedded == "y" ]; then
printf "\nDetected embedded skipping venv\n"
else
printf "\nSkipping virtual environment...\n"
# install dependencies
printf "Are you on Raspberry Pi(debian/ubuntu)?\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
printf "\nDo you want to install the bot in a python virtual environment? (y/n)"
read venv
if [ $venv == "y" ]; then
# set virtual environment
if ! python3 -m venv --help &> /dev/null; then
printf "Python3/venv error, please install python3-venv with your OS\n"
exit 1
else
echo "The Following could be messy, or take some time on slower devices."
echo "Creating virtual environment..."
#check if python3 has venv module
if [ -f venv/bin/activate ]; then
printf "\nFound virtual environment for python\n"
python3 -m venv venv
source venv/bin/activate
else
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
sudo apt-get install python3-venv
fi
# create virtual environment
python3 -m venv venv
# double check for python3-venv
if [ -f venv/bin/activate ]; then
printf "\nFound virtual environment for python\n"
source venv/bin/activate
else
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
exit 1
fi
printf "\nVirtual environment created\n"
# config service files for virtual environment
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
sed -i "$replace" etc/mesh_bot.service
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
sed -i "$replace" etc/pong_bot.service
# install dependencies
pip install -U -r requirements.txt
fi
else
pip install -U -r requirements.txt
printf "\nSkipping virtual environment...\n"
# install dependencies
printf "Are you on Raspberry Pi(debian/ubuntu)?\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
else
pip install -U -r requirements.txt
fi
fi
fi
@@ -134,58 +158,75 @@ sudo systemctl daemon-reload
printf "\n service files updated\n"
if [ $bot == "pong" ]; then
echo "useradd -M meshbot"
echo "usermod -L meshbot"
echo "Added user meshbot with no home directory"
# install service for pong bot
sudo cp etc/pong_bot.service /etc/systemd/system/
sudo systemctl enable pong_bot.service
fi
if [ $bot == "mesh" ]; then
echo "useradd -M meshbot"
echo "usermod -L meshbot"
echo "Added user meshbot with no home directory"
# install service for mesh bot
sudo cp etc/mesh_bot.service /etc/systemd/system/
sudo systemctl enable mesh_bot.service
fi
if [ $bot == "n" ]; then
if [ -f launch.sh ]; then
printf "\nTo run the bot, use the command: ./launch.sh\n"
./launch.sh
# check if running on embedded
if [ $embedded == "n" ]; then
# ask if emoji font should be installed for linux
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [ $emoji == "y" ]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
fi
# ask if emoji font should be installed for linux
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
read emoji
if [ $emoji == "y" ]; then
sudo apt-get install -y fonts-noto-color-emoji
echo "Emoji font installed!, reboot to load the font"
fi
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
# ask if the user wants to install the LLM Ollama components
printf "\nDo you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if the user wants to install the LLM Ollama components
printf "\nDo you want to install the LLM Ollama components? (y/n)"
read ollama
if [ $ollama == "y" ]; then
curl -fsSL https://ollama.com/install.sh | sh
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [ $gemma == "y" ]; then
ollama pull gemma2:2b
# ask if want to install gemma2:2b
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
echo "Do you want to install the Gemma2:2b components? (y/n)"
read gemma
if [ $gemma == "y" ]; then
ollama pull gemma2:2b
fi
fi
fi
if [ $venv == "y" ]; then
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
fi
printf "\nGood time to reboot? (y/n)"
read reboot
if [ $reboot == "y" ]; then
sudo reboot
if [ $venv == "y" ]; then
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
fi
printf "\nGood time to reboot? (y/n)"
read reboot
if [ $reboot == "y" ]; then
sudo reboot
fi
else
# we are on embedded
# replace "type = serial" with "type = tcp" in config.ini
replace="s|type = serial|type = tcp|g"
sed -i "$replace" config.ini
# replace "# hostname = 192.168.0.1" with "hostname = localhost" in config.ini
replace="s|# hostname = 192.168.0.1|hostname = localhost|g"
sed -i "$replace" config.ini
printf "\nConfig file updated for embedded\n"
# Set up the meshing around service
#sudo cp /opt/meshing-around/meshing-around.service /etc/systemd/system/meshing-around.service
#sudo systemctl enable meshing-around.service
fi
printf "\nInstallation complete!\n"

View File

@@ -14,8 +14,6 @@ restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golf
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
DEBUGpacket = False # Debug print the packet rx
DEBUGhops = False # Debug print hop info and bad hop count packets
@@ -25,7 +23,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
message_lower = message.lower()
bot_response = "🤖I'm sorry, I'm afraid I can't do that."
# Command List
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
default_commands = {
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
@@ -45,8 +43,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"ea": lambda: handle_fema_alerts(message, message_from_id, deviceID),
"ealert": lambda: handle_fema_alerts(message, message_from_id, deviceID),
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
"email:": lambda: handle_email(message_from_id, message),
"games": lambda: gamesCmdList,
"globalthermonuclearwar": lambda: handle_gTnW(),
@@ -64,6 +62,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"readnews": lambda: read_news(),
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
"setemail": lambda: handle_email(message_from_id, message),
@@ -72,6 +71,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"sms:": lambda: handle_sms(message_from_id, message),
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
@@ -182,35 +182,36 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
multiPingList.pop(i)
msg = "🛑 auto-ping"
# disabled in channel
if autoPingInChannel and not isDM:
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
msg = "🔊AutoPing via DM only⛔"
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not isDM:
msg = get_name_from_number(message_from_id) + msg
if not useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
@@ -281,9 +282,9 @@ def handle_wxalert(message_from_id, deviceID, message):
location = get_node_location(message_from_id, deviceID)
if "wxalert" in message:
# Detailed weather alert
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
weatherAlert = getActiveWeatherAlertsDetailNOAA(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
weatherAlert = getWeatherAlertsNOAA(str(location[0]), str(location[1]))
if NO_ALERTS not in weatherAlert:
weatherAlert = weatherAlert[0]
@@ -645,29 +646,56 @@ def handleGolf(message, nodeID, deviceID):
time.sleep(responseDelay + 1)
return msg
def handle_riverFlow(message, message_from_id, deviceID):
location = get_node_location(message_from_id, deviceID)
userRiver = message.lower()
if "riverflow " in userRiver:
userRiver = userRiver.split("riverflow ")[1] if "riverflow " in userRiver else riverListDefault
else:
userRiver = userRiver.split(",") if "," in userRiver else riverListDefault
# return river flow data
if use_meteo_wxApi:
return get_flood_openmeteo(location[0], location[1])
else:
# if userRiver a list
if type(userRiver) == list:
msg = ""
for river in userRiver:
msg += get_flood_noaa(location[0], location[1], river)
return msg
# if single river
msg = get_flood_noaa(location[0], location[1], userRiver)
return msg
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("System: Bot Returning Open-Meteo API for weather imperial")
#logger.debug("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("System: Bot Returning Open-Meteo API for weather metric")
#logger.debug("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("System: Bot Returning NOAA API for weather metric")
weather = get_weather(str(location[0]), str(location[1]), 1)
#logger.debug("System: Bot Returning NOAA API for weather metric")
weather = get_NOAAweather(str(location[0]), str(location[1]), 1)
else:
logger.debug("System: Bot Returning NOAA API for weather imperial")
weather = get_weather(str(location[0]), str(location[1]))
#logger.debug("System: Bot Returning NOAA API for weather imperial")
weather = get_NOAAweather(str(location[0]), str(location[1]))
return weather
def handle_fema_alerts(message, message_from_id, deviceID):
def handle_emergency_alerts(message, message_from_id, deviceID):
location = get_node_location(message_from_id, deviceID)
if enableGBalerts:
# UK Alerts
return get_govUK_alerts(str(location[0]), str(location[1]))
if message.lower().startswith("ealert"):
# Detailed alert
# Detailed alert FEMA
return getIpawsAlert(str(location[0]), str(location[1]))
else:
# Headlines only
# Headlines only FEMA
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
def handle_bbspost(message, message_from_id, deviceID):
@@ -736,6 +764,12 @@ 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 sysinfo(message, message_from_id, deviceID):
if "?" in message:
return "sysinfo command returns system information."
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
@@ -826,7 +860,7 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
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]))
return get_NOAAtide(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)
@@ -978,7 +1012,7 @@ def onReceive(packet, interface):
rxNode = 1
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
@@ -1008,6 +1042,10 @@ def onReceive(packet, interface):
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
snr = packet.get('rxSnr', 0)
@@ -1179,7 +1217,7 @@ def onReceive(packet, interface):
send_message(rMsg, channel_number, 0, 1)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
consumeMetadata(packet, rxNode)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
logger.debug(f"System: Error Packet = {packet}")
@@ -1240,6 +1278,8 @@ async def start_rx():
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
if wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
if emergencyAlertBrodcastEnabled:
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
if emergency_responder_enabled:
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
if enableSMTP:

View File

@@ -3,22 +3,31 @@
from modules.log import *
import asyncio
import random
import os
trap_list_filemon = ("readnews",)
def read_file(file_monitor_file_path):
def read_file(file_monitor_file_path, random_line_only=False):
try:
with open(file_monitor_file_path, 'r') as f:
content = f.read()
return content
if random_line_only:
# read a random line from the file
with open(file_monitor_file_path, 'r') as f:
lines = f.readlines()
return random.choice(lines)
else:
# read the whole file
with open(file_monitor_file_path, 'r') as f:
content = f.read()
return content
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
def read_news():
# read the news file on demand
return read_file(news_file_path)
return read_file(news_file_path, read_news_enabled)
def write_news(content, append=False):
# write the news file on demand

73
modules/gpio.py Normal file
View File

@@ -0,0 +1,73 @@
# GPIO module for MeshLink, concept code, not implemented
# K7MHI Kelly Keeton 2024
# https://pypi.org/project/gpio/
#import gpio
# https://pythonhosted.org/RPIO/
import RPIO
from modules.log import *
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
# set up input channel without pull-up
RPIO.setup(7, RPIO.IN)
# set up input channel with pull-up
RPIO.setup(8, RPIO.IN, pull_up_down=RPIO.PUD_UP)
# set up GPIO output channel
RPIO.setup(8, RPIO.OUT)
# change to BOARD numbering schema
RPIO.setmode(RPIO.BOARD)
# set up PWM channel
RPIO.setup(12, RPIO.OUT)
p = RPIO.PWM(12)
def gpio_status():
# get status of GPIO pins
gpio_status = ""
gpio_status += "GPIO 7: " + str(RPIO.input(7)) + "\n"
gpio_status += "GPIO 8: " + str(RPIO.input(8)) + "\n"
gpio_status += "GPIO 12: " + str(RPIO.input(12)) + "\n"
return gpio_status
def gpio_toggle():
# toggle GPIO pin 8
RPIO.output(8, not RPIO.input(8))
return "GPIO 8 toggled"
def gpio_pwm():
# set PWM on GPIO pin 12
p.start(50)
return "PWM started"
def gpio_stop():
# stop PWM on GPIO pin 12
p.stop()
return "PWM stopped"
def gpio_shutdown():
# shutdown GPIO
RPIO.cleanup()
return "GPIO shutdown"
def trap_gpio(message):
# trap for GPIO commands
if "status" in message:
return gpio_status()
elif "toggle" in message:
return gpio_toggle()
elif "pwm" in message:
return gpio_pwm()
elif "stop" in message:
return gpio_stop()
elif "shutdown" in message:
return gpio_shutdown()
else:
return "GPIO command not recognized"

View File

@@ -1,4 +1,4 @@
# helper functions to use location data like NOAA weather
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
# K7MHI Kelly Keeton 2024
import json # pip install json
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert")
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
@@ -154,9 +154,8 @@ def getArtSciRepeaters(lat=0, lon=0):
else:
msg = f"no results.. sorry"
return msg
def get_tide(lat=0, lon=0):
def get_NOAAtide(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")
@@ -172,7 +171,7 @@ def get_tide(lat=0, lon=0):
if station_json['stationList'] == [] or station_json['stationList'] is None:
logger.error("Location:No tide station found")
return ERROR_FETCHING_DATA
return "No tide station found with info provided"
station_id = station_json['stationList'][0]['stationId']
@@ -219,7 +218,7 @@ def get_tide(lat=0, lon=0):
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
def get_NOAAweather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
weather = ""
if float(lat) == 0 and float(lon) == 0:
@@ -263,7 +262,7 @@ def get_weather(lat=0, lon=0, unit=0):
weather = weather[:-1]
# get any alerts and return the count
alerts = getWeatherAlerts(lat, lon)
alerts = getWeatherAlertsNOAA(lat, lon)
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
alert = ""
@@ -333,7 +332,7 @@ def abbreviate_noaa(row):
return line
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
@@ -381,26 +380,27 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
return data
wxAlertCache = ""
def alertBrodcast():
wxAlertCacheNOAA = ""
def alertBrodcastNOAA():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
global wxAlertCacheNOAA
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
# check if any reason to discard the alerts
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
return False
elif currentAlert == NO_ALERTS:
wxAlertCache = ""
wxAlertCacheNOAA = ""
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
elif currentAlert[0] not in wxAlertCacheNOAA:
# Check if the current alert is not in the weather alert cache
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
wxAlertCacheNOAA = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
if float(lat) == 0 and float(lon) == 0:
@@ -560,3 +560,53 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
alert = NO_ALERTS
return alert
def get_flood_noaa(lat=0, lon=0, uid=0):
# get the latest flood alert from NOAA
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
headers = {'accept': 'application/json'}
if uid == 0:
return "No flood gauge data found"
try:
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
if not response.ok:
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
data = response.json()
if not data:
return "No flood gauge data found"
# extract values from JSON
try:
name = data['name']
status_observed_primary = data['status']['observed']['primary']
status_observed_primary_unit = data['status']['observed']['primaryUnit']
status_observed_secondary = data['status']['observed']['secondary']
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
status_observed_floodCategory = data['status']['observed']['floodCategory']
status_forecast_primary = data['status']['forecast']['primary']
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
status_forecast_secondary = data['status']['forecast']['secondary']
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
# except KeyError as e:
# print(f"Missing key in data: {e}")
# except TypeError as e:
# print(f"Type error in data: {e}")
except Exception as e:
logger.warning("Location:Error extracting flood gauge data from NOAA for " + str(uid))
return ERROR_FETCHING_DATA
# format the flood data
logger.debug(f"System: NOAA Flood data for {str(uid)}")
flood_data = f"Flood Data {name}:\n"
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
return flood_data

View File

@@ -0,0 +1,60 @@
# helper functions to use location data for data outside US/north america
# K7MHI Kelly Keeton 2024
import json # pip install json
from geopy.geocoders import Nominatim # pip install geopy
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.log import *
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
def get_govUK_alerts(shortAlerts=False):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'
response = requests.get(url)
soup = bs.BeautifulSoup(response.text, 'html.parser')
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
except Exception as e:
logger.warning("Error getting UK alerts: " + str(e))
return NO_ALERTS
if alert:
return "🚨" + alert.get_text(strip=True)
else:
return NO_ALERTS
def get_wxUKgov():
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
try:
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
response = requests.get(url)
soup = bs.BeautifulSoup(response.content, 'xml')
items = soup.find_all('item')
alerts = []
for item in items:
title = item.find('title').get_text(strip=True)
description = item.find('description').get_text(strip=True)
alerts.append(f"🚨 {title}: {description}")
return "\n".join(alerts) if alerts else NO_ALERTS
except Exception as e:
logger.warning("Error getting UK weather warnings: " + str(e))
return NO_ALERTS
def get_floodUKgov():
# get UK flood warnings
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
return NO_ALERTS

View File

@@ -28,6 +28,8 @@ retry_int2 = False
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -154,25 +156,22 @@ try:
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
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
mySAME = config['location'].get('mySAME', '').split(',') # default empty
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
femaAlertBroadcastEnabled = config['location'].getboolean('femaAlertBroadcastEnabled', False) # default False
femaAlertBroadcastCh = config['location'].get('femaAlertBroadcastCh', '2').split(',') # default Channel 2
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if wxAlertBroadcastChannel:
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
# location alerts
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
mySAME = config['location'].get('mySAME', '').split(',') # default empty
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
@@ -221,6 +220,7 @@ try:
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops

View File

@@ -9,7 +9,7 @@ import ephem # pip install pyephem
from datetime import timedelta
from modules.log import *
trap_list_solarconditions = ("sun", "solar", "hfcond", "satpass")
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
def hf_band_conditions():
# ham radio HF band conditions

View File

@@ -18,7 +18,6 @@ asyncLoop = asyncio.new_event_loop()
games_enabled = False
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
# Ping Configuration
if ping_enabled:
# ping, pinging, ack, testing, test, pong
@@ -28,9 +27,9 @@ if ping_enabled:
# Sitrep Configuration
if sitrep_enabled:
trap_list_sitrep = ("sitrep", "lheard")
trap_list_sitrep = ("sitrep", "lheard", "sysinfo")
trap_list = trap_list + trap_list_sitrep
help_message = help_message + ", sitrep"
help_message = help_message + ", sitrep, sysinfo"
# MOTD Configuration
if motd_enabled:
@@ -75,6 +74,10 @@ 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, rlist"
if enableGBalerts:
from modules.locationdata_eu import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_location_eu
#help_message = help_message + ", ukalert, ukwx, ukflood"
# Open-Meteo Configuration for worldwide weather
if use_meteo_wxApi:
@@ -191,7 +194,7 @@ if file_monitor_enabled or read_news_enabled:
from modules.filemon import * # from the spudgunman/meshing-around repo
if read_news_enabled:
trap_list = trap_list + trap_list_filemon # items readnews
help_message = help_message + ", readmail"
help_message = help_message + ", readnews"
# clean up the help message
help_message = help_message.split(", ")
@@ -662,7 +665,7 @@ def handleMultiPing(nodeID=0, deviceID=1):
break
def handleWxBroadcast(deviceID=1):
def handleAlertBroadcast(deviceID=1):
# only allow API call every 20 minutes
# the watchdog will call this function 3 times, seeing possible throttling on the API
clock = datetime.now()
@@ -672,15 +675,50 @@ def handleWxBroadcast(deviceID=1):
return False
# check for alerts
alert = alertBrodcast()
if alert:
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
if isinstance(wxAlertBroadcastChannel, list):
for channel in wxAlertBroadcastChannel:
send_message(msg, int(channel), 0, deviceID)
else:
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
return True
alertWx = alertBrodcastNOAA()
alertFema = getIpawsAlert(latitudeValue,longitudeValue, shortAlerts=True)
if enableGBalerts:
alertUk = get_govUK_alerts()
else:
alertUk = NO_ALERTS
# format alert
if alertWx:
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
else:
wxAlert = False
femaAlert = alertFema
ukAlert = alertUk
if emergencyAlertBrodcastEnabled:
if NO_ALERTS not in femaAlert:
if isinstance(emergencyAlertBroadcastCh, list):
for channel in emergencyAlertBroadcastCh:
send_message(femaAlert, int(channel), 0, deviceID)
else:
send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID)
return True
if NO_ALERTS not in ukAlert:
if isinstance(emergencyAlertBroadcastCh, list):
for channel in emergencyAlertBroadcastCh:
send_message(ukAlert, int(channel), 0, deviceID)
else:
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
return True
# pause for 10 seconds
time.sleep(10)
if wxAlertBroadcastEnabled:
if wxAlert:
if isinstance(wxAlertBroadcastChannel, list):
for channel in wxAlertBroadcastChannel:
send_message(wxAlert, int(channel), 0, deviceID)
else:
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
return True
def onDisconnect(interface):
global retry_int1, retry_int2
@@ -743,7 +781,7 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
# Create a StringIO object to capture the
output_capture = io.StringIO()
with contextlib.redirect_stdout(output_capture):
with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture):
interface.localNode.getMetadata()
console_output = output_capture.getvalue()
if "firmware_version" in console_output:
@@ -751,22 +789,22 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
return fwVer
return -1
def displayNodeTelemetry(nodeID=0, rxNode=0):
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
interface = interface1 if rxNode == 1 else interface2
global telemetryData
# throttle the telemetry requests to prevent spamming the device
if rxNode == 1:
if time.time() - telemetryData[0]['interface1'] < 600:
if time.time() - telemetryData[0]['interface1'] < 600 and not userRequested:
return -1
telemetryData[0]['interface1'] = time.time()
elif rxNode == 2:
if time.time() - telemetryData[0]['interface2'] < 600:
if time.time() - telemetryData[0]['interface2'] < 600 and not userRequested:
return -1
telemetryData[0]['interface2'] = time.time()
# some telemetry data is not available in python-meshtastic?
# bring in values from the last telemetry request for the node
# bring in values from the last telemetry dump for the node
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
@@ -901,7 +939,20 @@ def consumeMetadata(packet, rxNode=0):
if debugMetadata: print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
# get the remote hardware data
remote_hardware_data = packet['decoded']['remoteHardware']
def get_sysinfo(nodeID=0, deviceID=1):
# Get the system telemetry data for return on the sysinfo command
sysinfo = ''
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
if "numPacketsRx:0" in stats or stats == -1:
return "Gathering Telemetry try again later⏳"
# replace Telemetry with Int in string
stats = stats.replace("Telemetry", "Int")
sysinfo += f"📊{stats}"
if interface2_enabled:
sysinfo += f"📊{stats}"
return sysinfo
async def BroadcastScheduler():
# handle schedule checks for the broadcast of messages
@@ -1074,8 +1125,10 @@ async def watchdog():
# multiPing handler
handleMultiPing(0,1)
if wxAlertBroadcastEnabled:
handleWxBroadcast(1)
# Alert Broadcast
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
# weather alerts
handleAlertBroadcast(1)
# Telemetry data
int1Data = displayNodeTelemetry(0, 1)
@@ -1104,10 +1157,12 @@ async def watchdog():
await handleSentinel(2)
# multiPing handler
handleMultiPing(0,2)
handleMultiPing(0,1)
if wxAlertBroadcastEnabled:
handleWxBroadcast(2)
# Alert Broadcast
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
# weather alerts
handleAlertBroadcast(1)
# Telemetry data
int2Data = displayNodeTelemetry(0, 2)

View File

@@ -1,18 +1,19 @@
import openmeteo_requests # pip install openmeteo-requests
from retry_requests import retry # pip install retry_requests
#import requests_cache
#import openmeteo_requests # pip install openmeteo-requests
#from retry_requests import retry # pip install retry_requests
import requests
import json
from modules.log import *
def get_weather_data(api_url, params):
response = requests.get(api_url, params=params)
response.raise_for_status() # Raise an error for bad status codes
return response.json()
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"
@@ -34,27 +35,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
try:
# Fetch the weather data
responses = openmeteo.weather_api(url, params=params)
weather_data = get_weather_data(url, 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 location
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
# Ensure response is defined
response = weather_data
# 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()
daily = response['daily']
daily_weather_code = daily['weather_code']
daily_temperature_2m_max = daily['temperature_2m_max']
daily_temperature_2m_min = daily['temperature_2m_min']
daily_precipitation_hours = daily['precipitation_hours']
daily_precipitation_probability_max = daily['precipitation_probability_max']
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
except Exception as e:
logger.error(f"Error processing meteo weather data: {e}")
return ERROR_FETCHING_DATA
@@ -191,3 +194,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
return weather_report
def get_flood_openmeteo(lat=0, lon=0):
# set forcast days 1 or 3
forecastDays = 3
# Flood data
url = "https://flood-api.open-meteo.com/v1/flood"
params = {
"latitude": {lat},
"longitude": {lon},
"timezone": "auto",
"daily": "river_discharge",
"forecast_days": forecastDays
}
try:
# Fetch the flood data
flood_data = get_weather_data(url, params)
except Exception as e:
logger.error(f"Error fetching meteo flood data: {e}")
return ERROR_FETCHING_DATA
# Check if we got a response
try:
# Process location
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
# Ensure response is defined
response = flood_data
# Process daily data. The order of variables needs to be the same as requested.
daily = response['daily']
daily_river_discharge = daily['river_discharge']
# check if none
except Exception as e:
logger.error(f"Error processing meteo flood data: {e}")
return ERROR_FETCHING_DATA
# create a flood report
flood_report = ""
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
return flood_report

View File

@@ -9,7 +9,6 @@ from modules.log import *
from modules.system import *
# Global Variables
cmdHistory = [] # list to hold the last commands
DEBUGpacket = False # Debug print the packet rx
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
@@ -18,6 +17,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
bot_response = "I'm sorry, I'm afraid I can't do that."
command_handler = {
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
@@ -29,6 +29,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
}
@@ -100,35 +101,36 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
multiPingList.pop(i)
msg = "🛑 auto-ping"
# disabled in channel
if autoPingInChannel and not isDM:
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
if len(multiPingList) > 2:
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
pingCount = -1
else:
msg = "🔊AutoPing via DM only⛔"
# set inital pingCount
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
elif not autoPingInChannel and not isDM:
# no autoping in channels
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
msg = f"🚦Initalizing {pingCount} auto-ping"
# if not a DM add the username to the beginning of msg
if not isDM:
msg = get_name_from_number(message_from_id) + msg
if not useDMForResponse and not isDM:
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
return msg
@@ -140,6 +142,12 @@ def handle_motd(message):
return "MOTD Set to: " + MOTD
else:
return MOTD
def sysinfo(message, message_from_id, deviceID):
if "?" in message:
return "sysinfo command returns system information."
else:
return get_sysinfo(message_from_id, deviceID)
def handle_lheard(message, nodeid, deviceID, isDM):
if "?" in message and isDM:
@@ -200,6 +208,9 @@ def onReceive(packet, interface):
elif interface2_enabled and interface2_type == 'ble':
rxNode = 2
# set the message_from_id
message_from_id = packet['from']
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
@@ -209,7 +220,10 @@ def onReceive(packet, interface):
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
message_from_id = packet['from']
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
@@ -333,7 +347,7 @@ def onReceive(packet, interface):
send_message(rMsg, channel_number, 0, 1)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
consumeMetadata(packet, rxNode)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
logger.debug(f"System: Error Packet = {packet}")

View File

@@ -6,9 +6,6 @@ requests
maidenhead
beautifulsoup4
dadjokes
openmeteo_requests
retry_requests
numpy
geopy
schedule
wikipedia