mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e21dbaab | ||
|
|
267f50c591 | ||
|
|
0013a7bb74 | ||
|
|
73fe8be432 | ||
|
|
3d45195ae9 | ||
|
|
ff390cf470 | ||
|
|
17d8cd1067 | ||
|
|
b9348c906d | ||
|
|
6ba3508cc5 | ||
|
|
1c78f154da | ||
|
|
e0a3d0f94e | ||
|
|
066211e9f2 | ||
|
|
5701cd108b | ||
|
|
b877a294ac | ||
|
|
2aedcfc46e | ||
|
|
12147db5d0 | ||
|
|
cef37b574b | ||
|
|
6f121b7aac | ||
|
|
9e31b7f47e | ||
|
|
f3103984ef | ||
|
|
9c8b3f0a54 | ||
|
|
f88cbf210e | ||
|
|
9909113beb | ||
|
|
c1b783b1cd | ||
|
|
9b3b6a5d3d | ||
|
|
cffdb3c089 | ||
|
|
7bb9c9ac55 | ||
|
|
830ec95080 | ||
|
|
0ea575ac70 | ||
|
|
d836255716 | ||
|
|
4f115c9c21 | ||
|
|
63bd5b836d | ||
|
|
5ad9b9a261 | ||
|
|
7a024b681f | ||
|
|
75df5a695b | ||
|
|
0ef8cffd56 | ||
|
|
73e8e063d2 | ||
|
|
82880677f4 | ||
|
|
fe8ba8aaf4 | ||
|
|
cea9147745 | ||
|
|
c1c68d4c10 | ||
|
|
5fcd21680e | ||
|
|
9e1356172f | ||
|
|
de7fdfad11 | ||
|
|
a87055874a | ||
|
|
5c7433091d | ||
|
|
f0ca818461 | ||
|
|
76006dcda7 | ||
|
|
33abe646ae | ||
|
|
c47004c47c | ||
|
|
e66d945be7 | ||
|
|
10afc128f4 | ||
|
|
e6fc794951 | ||
|
|
4839e9ba03 | ||
|
|
bde15e311a | ||
|
|
21c83222e9 | ||
|
|
bbcdd6656a | ||
|
|
7f61b86252 | ||
|
|
25ae27a162 | ||
|
|
a04133e82f | ||
|
|
2a9dfc90ee | ||
|
|
f1bf84f6f0 | ||
|
|
4b91ef10b4 | ||
|
|
cd4497b129 | ||
|
|
01374a8307 | ||
|
|
46c115b783 | ||
|
|
eec7230a84 | ||
|
|
9394fd6ca9 | ||
|
|
c6653da1f3 | ||
|
|
9f47958a03 | ||
|
|
78e51b7be1 | ||
|
|
26fcf6fc02 | ||
|
|
c2336850fe | ||
|
|
54e0d17e70 | ||
|
|
7a6d1f7b29 | ||
|
|
7e26d3f0e5 |
@@ -1 +1,2 @@
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
there is some ideas for adding code in modules/README.md
|
||||
53
README.md
53
README.md
@@ -11,13 +11,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
|
||||
- **New Node Hello**: Greet new nodes on the mesh with a hello message
|
||||
|
||||
### Network Tools
|
||||
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
|
||||
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
### Multi Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
@@ -26,7 +27,8 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visability.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
@@ -52,7 +54,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
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
@@ -65,7 +67,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, 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.
|
||||
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), also see [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
|
||||
|
||||
@@ -76,14 +78,14 @@ git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
The code is under active development, so make sure to pull the latest changes regularly!
|
||||
|
||||
#### Automation of setup
|
||||
#### Quick setup
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
|
||||
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
#### Docker Installation
|
||||
If you prefer to use [Docker](script/docker/README.md)
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
#### Custom Install
|
||||
#### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -94,8 +96,10 @@ Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
@@ -203,8 +207,8 @@ 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 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
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
@@ -213,7 +217,10 @@ 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
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
@@ -308,7 +315,14 @@ rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas
|
||||
#### Newspaper on mesh
|
||||
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
|
||||
|
||||
you can also enable the line by line (hint just search for the commented lines with a 🐝) to return a string from the [bee movie](https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt) for example adding it alongside news.txt as bee.txt
|
||||
### Greet new nodes QRZ module
|
||||
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
|
||||
```ini
|
||||
[qrz]
|
||||
enabled = True # QRZ Hello to new nodes
|
||||
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
@@ -362,7 +376,7 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
@@ -399,9 +413,10 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
| `checkin` | Check in the node to the checklist database | ✅ |
|
||||
| `checkout` | Checkout the node in the checklist database | ✅ |
|
||||
| `checklist` | Display the checklist database | ✅ |
|
||||
|---------|-------------|-
|
||||
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
|
||||
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
|
||||
| `checklist` | Display the checklist database, with note | ✅ |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|
||||
@@ -123,8 +123,8 @@ useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# NOAA weather forecast days
|
||||
NOAAforecastDuration = 3
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
|
||||
@@ -153,9 +153,6 @@ ignoreFEMAtest = True
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
|
||||
# Use UK Alert Broadcast Data
|
||||
enableGBalerts = False
|
||||
|
||||
# Use DE Alert Broadcast Data
|
||||
enableDEalerts = False
|
||||
# comma separated list of regional codes trigger local alert.
|
||||
@@ -172,6 +169,15 @@ satList = 25544,7530
|
||||
[checklist]
|
||||
enabled = False
|
||||
checklist_db = data/checklist.db
|
||||
reverse_in_out = False
|
||||
|
||||
[qrz]
|
||||
# QRZ Hello to new nodes with message
|
||||
enabled = False
|
||||
qrz_db = data/qrz.db
|
||||
qrz_hello_string = "send CMD or DM me for more info."
|
||||
# Training mode will not send the hello message to new nodes
|
||||
training = True
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot.service
|
||||
# sudo systemctl start mesh_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl start mesh_bot.service
|
||||
# sudo systemctl enable mesh_bot_reporting.service
|
||||
# sudo systemctl start mesh_bot_reporting.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-Reporting
|
||||
|
||||
23
etc/mesh_bot_w3.tmp
Normal file
23
etc/mesh_bot_w3.tmp
Normal file
@@ -0,0 +1,23 @@
|
||||
# /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot_w3.service
|
||||
# sudo systemctl start mesh_bot_w3.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-W3Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable pong_bot.service
|
||||
# sudo systemctl start pong_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
42
install.sh
42
install.sh
@@ -80,6 +80,7 @@ sudo usermod -a -G bluetooth $USER
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [[ -f config.ini ]]; then
|
||||
@@ -158,10 +159,10 @@ else
|
||||
fi
|
||||
|
||||
# if $1 is passed
|
||||
if [[ $1 == "mesh" ]]; then
|
||||
bot="mesh"
|
||||
elif [[ $1 == "pong" ]]; then
|
||||
if [[ $1 == "pong" ]]; then
|
||||
bot="pong"
|
||||
elif [[ $1 == "mesh" ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
bot="mesh"
|
||||
else
|
||||
printf "\n\n"
|
||||
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
|
||||
@@ -176,6 +177,7 @@ replace="s|/dir/|$program_path/|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
# set the correct user in the service file?
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
@@ -207,10 +209,12 @@ replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
printf "\n service files updated\n"
|
||||
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
@@ -260,19 +264,18 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
fi
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
@@ -294,6 +297,8 @@ else
|
||||
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nConfig file updated for embedded\n"
|
||||
# add service dependency for meshtasticd into service file
|
||||
#replace="s|After=network.target|After=network.target meshtasticd.service|g"
|
||||
|
||||
# Set up the meshing around service
|
||||
sudo cp /opt/meshing-around/etc/$service.service /etc/systemd/system/$service.service
|
||||
@@ -301,7 +306,9 @@ else
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
printf "Reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
@@ -322,6 +329,7 @@ exit 0
|
||||
# sudo systemctl disable mesh_bot_reporting
|
||||
# sudo rm /etc/systemd/system/mesh_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo rm /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl reset-failed
|
||||
|
||||
@@ -4,6 +4,7 @@ Logs will collect here. Give a day of logs or a bunch of messages to have good r
|
||||
## Reporting Note
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
|
||||
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
|
||||
- If you are in a venv and using launch.sh you can `launch.sh html5`
|
||||
|
||||

|
||||
|
||||
|
||||
91
mesh_bot.py
91
mesh_bot.py
@@ -43,10 +43,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
|
||||
"checkin": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checklist": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"clearsms": lambda: handle_sms(message_from_id, message),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"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),
|
||||
@@ -140,6 +140,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
time.sleep(responseDelay)
|
||||
return bot_response
|
||||
|
||||
def handle_cmd(message, message_from_id, deviceID):
|
||||
# why CMD? its just a command list. a terminal would normally use "Help"
|
||||
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
|
||||
@@ -341,8 +348,9 @@ def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
return passes
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory, seenNodes
|
||||
location_name = 'no location provided'
|
||||
msg = ''
|
||||
|
||||
if location_enabled:
|
||||
# if message_from_id is is the llmLocationTable use the location from the list to save on API calls
|
||||
@@ -368,17 +376,20 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# consider this a command use for the cmdHistory list
|
||||
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
|
||||
|
||||
# if the message_from_id is not in the llmLocationTable send the welcome message
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# check for a welcome message (is this redundant?)
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
node['welcome'] = True
|
||||
|
||||
# update the llmLocationTable for future use
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
@@ -397,26 +408,18 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# information for the user on how long the query will take on average
|
||||
if llmRunCounter > 0:
|
||||
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
|
||||
if averageRuntime > 25:
|
||||
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
msg = f"Average query time is: {int(averageRuntime)} seconds" if averageRuntime > 25 else ''
|
||||
else:
|
||||
msg = "Please wait, response could take 30+ seconds. Fund the SysOp's GPU budget!"
|
||||
|
||||
if msg != '':
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
start = time.time()
|
||||
|
||||
@@ -700,9 +703,6 @@ def handle_emergency_alerts(message, message_from_id, deviceID):
|
||||
if enableDEalerts:
|
||||
# nina Alerts
|
||||
return get_nina_alerts()
|
||||
if enableGBalerts:
|
||||
# UK Alerts
|
||||
return get_govUK_alerts(str(location[0]), str(location[1]))
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
@@ -976,13 +976,14 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
game = "None"
|
||||
|
||||
trackers = [
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars),
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade),
|
||||
(vpTracker, "VideoPoker", handleVideoPoker),
|
||||
(jackTracker, "BlackJack", handleBlackJack),
|
||||
(mindTracker, "MasterMind", handleMmind),
|
||||
(golfTracker, "GolfSim", handleGolf),
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
|
||||
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
]
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
|
||||
@@ -1197,6 +1198,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for us to respond to
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
else:
|
||||
@@ -1219,7 +1221,7 @@ def onReceive(packet, interface):
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
|
||||
|
||||
else:
|
||||
# message is not for bot to respond to
|
||||
# message is not for us to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -1254,11 +1256,14 @@ def onReceive(packet, interface):
|
||||
# if QRZ enabled check if we have said hello
|
||||
if qrz_hello_enabled:
|
||||
if never_seen_before(message_from_id):
|
||||
name = {get_name_from_number(message_from_id, 'short', rxNode)}
|
||||
# add to qrz_hello list
|
||||
hello(message_from_id, get_name_from_number(message_from_id, 'short', rxNode))
|
||||
# send a hello message
|
||||
send_message(f"Hello {get_name_from_number(message_from_id, 'short', rxNode)}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
@@ -1334,8 +1339,10 @@ async def start_rx():
|
||||
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 qrz_hello_enabled:
|
||||
logger.debug(f"System: QRZ Hello Enabled")
|
||||
if qrz_hello_enabled and train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled with training mode")
|
||||
if qrz_hello_enabled and not train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled")
|
||||
if checklist_enabled:
|
||||
logger.debug(f"System: CheckList Module Enabled")
|
||||
if enableSMTP:
|
||||
|
||||
42
modules/README.md
Normal file
42
modules/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Modules and Adding stuff
|
||||
|
||||
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
|
||||
|
||||
## By following these steps, you can add a new bbs option to the bot.
|
||||
|
||||
1. **Define the Command Handler**:
|
||||
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
|
||||
```python
|
||||
def handle_newcommand(message, message_from_id, deviceID):
|
||||
return "This is a response from the new command."
|
||||
```
|
||||
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
|
||||
2. **Add the Command to the Auto Response**:
|
||||
Update the auto_response function in mesh_bot.py to include the new command:
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
|
||||
#...
|
||||
```
|
||||
3. **Update the Trap List and Help**:
|
||||
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
|
||||
```python
|
||||
#...
|
||||
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
|
||||
help_message = "Bot CMD?:newcommand, "
|
||||
#...
|
||||
```
|
||||
|
||||
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
|
||||
```python
|
||||
# newcommand Configuration
|
||||
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
|
||||
if newcommand_enabled:
|
||||
trap_list_newcommand = ("newcommand",)
|
||||
trap_list = trap_list + trap_list_newcommand
|
||||
help_message = help_message + ", newcommand"
|
||||
```
|
||||
|
||||
5. **Test the New Command**:
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
@@ -28,6 +28,8 @@ def checkin(name, date, time, location, notes):
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
# # remove any checkouts that are older than the checkin
|
||||
# c.execute("DELETE FROM checkout WHERE checkout_date < ? OR (checkout_date = ? AND checkout_time < ?)", (date, date, time))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
@@ -36,7 +38,10 @@ def checkin(name, date, time, location, notes):
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checked in: " + str(name)
|
||||
if reverse_in_out:
|
||||
return "Checked✅Out: " + str(name)
|
||||
else:
|
||||
return "Checked✅In: " + str(name)
|
||||
|
||||
def delete_checkin(checkin_id):
|
||||
# delete a checkin
|
||||
@@ -47,22 +52,50 @@ def delete_checkin(checkin_id):
|
||||
conn.close()
|
||||
return "Checkin deleted." + str(checkin_id)
|
||||
|
||||
def checkout(name, date, time, location, notes):
|
||||
def checkout(name, date, time_str, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
# checkout a user
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
# Check if the user has a checkin before checking out
|
||||
c.execute("""
|
||||
SELECT checkin_id FROM checkin
|
||||
WHERE checkin_name = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM checkout
|
||||
WHERE checkout_name = checkin_name
|
||||
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
|
||||
)
|
||||
ORDER BY checkin_date DESC, checkin_time DESC
|
||||
LIMIT 1
|
||||
""", (name,))
|
||||
checkin_record = c.fetchone()
|
||||
if checkin_record:
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
# calculate length of time checked in
|
||||
c.execute("SELECT checkin_time FROM checkin WHERE checkin_id = ?", (checkin_record[0],))
|
||||
checkin_time = c.fetchone()[0]
|
||||
checkin_datetime = time.strptime(date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
|
||||
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds))
|
||||
# # remove the checkin record older than the checkout
|
||||
# c.execute("DELETE FROM checkin WHERE checkin_date < ? OR (checkin_date = ? AND checkin_time < ?)", (date, date, time_str))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checked out: " + str(name)
|
||||
if checkin_record:
|
||||
if reverse_in_out:
|
||||
return "Checked⌛️In: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "Checked⌛️Out: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "None found for " + str(name)
|
||||
|
||||
def delete_checkout(checkout_id):
|
||||
# delete a checkout
|
||||
@@ -86,9 +119,16 @@ def list_checkin():
|
||||
""")
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
timeCheckedIn = ""
|
||||
checkin_list = ""
|
||||
for row in rows:
|
||||
checkin_list += "Checkin ID: " + row[1] + " Date: " + row[2] + " Time: " + row[3] + " Notes: " + row[5] + "\n"
|
||||
#calculate length of time checked in
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))))
|
||||
checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn
|
||||
if row[5] != "":
|
||||
checkin_list += "📝" + row[5]
|
||||
if row != rows[-1]:
|
||||
checkin_list += "\n"
|
||||
# if empty list
|
||||
if checkin_list == "":
|
||||
return "No data to display."
|
||||
@@ -97,14 +137,18 @@ def list_checkin():
|
||||
def process_checklist_command(nodeID, message, name="none", location="none"):
|
||||
current_date = time.strftime("%Y-%m-%d")
|
||||
current_time = time.strftime("%H:%M:%S")
|
||||
# if user on bbs_ban_list reject command
|
||||
if str(nodeID) in bbs_ban_list:
|
||||
logger.warning("System: Checklist attempt from the ban list")
|
||||
return "unable to process command"
|
||||
try:
|
||||
comment = message.split(" ", 1)[1]
|
||||
except IndexError:
|
||||
comment = ""
|
||||
# handle checklist commands
|
||||
if "checkin" in message.lower():
|
||||
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
|
||||
return checkin(name, current_date, current_time, location, comment)
|
||||
elif "checkout" in message.lower():
|
||||
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
|
||||
return checkout(name, current_date, current_time, location, comment)
|
||||
elif "purgein" in message.lower():
|
||||
return delete_checkin(nodeID)
|
||||
|
||||
@@ -12,7 +12,7 @@ from modules.log import *
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
trap_list_location_de = ("dealert", "dewx", "deflood")
|
||||
|
||||
def get_govUK_alerts(shortAlerts=False):
|
||||
def get_govUK_alerts(lat, lon):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
|
||||
@@ -211,6 +211,10 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
|
||||
@@ -71,7 +71,16 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
def getRepeaterBook(lat=0, lon=0):
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
data = []
|
||||
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
# check if in the US or not
|
||||
usapi ="https://www.repeaterbook.com/repeaters/prox_result.php?"
|
||||
elsewhereapi = "https://www.repeaterbook.com/row_repeaters/prox2_result.php?"
|
||||
if grid[:2] in ['CN', 'DN', 'EN', 'FN', 'CM', 'DM', 'EM', 'FM', 'DL', 'EL', 'FL']:
|
||||
repeater_url = usapi
|
||||
else:
|
||||
repeater_url = elsewhereapi
|
||||
|
||||
repeater_url += f"city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
|
||||
try:
|
||||
msg = ''
|
||||
response = requests.get(repeater_url)
|
||||
@@ -95,10 +104,8 @@ def getRepeaterBook(lat=0, lon=0):
|
||||
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
|
||||
}
|
||||
data.append(repeater)
|
||||
else:
|
||||
msg = "bug?Not enough columns"
|
||||
else:
|
||||
msg = "bug?Table not found"
|
||||
msg = "No Data for your Region"
|
||||
except Exception as e:
|
||||
msg = "No repeaters found 😔"
|
||||
# Limit the output to the first 4 repeaters
|
||||
@@ -229,36 +236,36 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
unit = 1
|
||||
|
||||
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
|
||||
if unit == 1:
|
||||
weather_url += "&unit=1"
|
||||
|
||||
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
|
||||
# extract the "forecast": property from the JSON response
|
||||
try:
|
||||
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
|
||||
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
|
||||
if not weather_data.ok:
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
# get the forecast URL from the JSON response
|
||||
weather_json = weather_data.json()
|
||||
forecast_url = weather_json['properties']['forecast']
|
||||
try:
|
||||
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
|
||||
if not forecast_data.ok:
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
|
||||
table = soup.find('div', id="detailed-forecast-body")
|
||||
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
|
||||
forecast_json = forecast_data.json()
|
||||
forecast = forecast_json['properties']['periods']
|
||||
for day in forecast[:forecastDuration]:
|
||||
# abreviate the forecast
|
||||
|
||||
if table is None:
|
||||
logger.error("Location:Bad weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
# get rows
|
||||
rows = table.find_all('div', class_="row")
|
||||
|
||||
# extract data from rows
|
||||
for row in rows:
|
||||
# shrink the text
|
||||
line = abbreviate_noaa(row.text)
|
||||
# only grab a few days of weather
|
||||
if len(weather.split("\n")) < forecastDuration:
|
||||
weather += line + "\n"
|
||||
# trim off last newline
|
||||
weather += day['name'] + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
|
||||
# remove last newline
|
||||
weather = weather[:-1]
|
||||
|
||||
# get any alerts and return the count
|
||||
@@ -323,6 +330,9 @@ def abbreviate_noaa(row):
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
}
|
||||
|
||||
line = row
|
||||
|
||||
@@ -18,19 +18,37 @@ def never_seen_before(nodeID):
|
||||
# check if we have seen this node before and sent a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
try:
|
||||
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row is None:
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
# we have seen this node before
|
||||
return False
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
logger.warning("QRZ database table not found, created new table")
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
|
||||
def hello(nodeID, name, qth, notes):
|
||||
def hello(nodeID, name):
|
||||
# send a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name, qrz_qth, qrz_notes) VALUES (?, ?, ?, ?)", (nodeID, name, qth, notes))
|
||||
try:
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
@@ -96,7 +96,7 @@ if 'checklist' not in config:
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'qrz' not in config:
|
||||
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db'}
|
||||
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
@@ -272,10 +272,13 @@ try:
|
||||
# checklist
|
||||
checklist_enabled = config['checklist'].getboolean('enabled', False)
|
||||
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
|
||||
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
|
||||
|
||||
# qrz hello
|
||||
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
|
||||
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
|
||||
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'send CMD or DM me for more info.')
|
||||
train_qrz = config['qrz'].getboolean('training', True)
|
||||
|
||||
# E-Mail Settings
|
||||
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
||||
|
||||
@@ -77,7 +77,7 @@ if location_enabled:
|
||||
help_message = help_message + ", whereami, wx, wxc, rlist"
|
||||
if enableGBalerts and not enableDEalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_eu
|
||||
logger.warning(f"System: GB Alerts not functional at this time need to find a source API")
|
||||
#help_message = help_message + ", ukalert, ukwx, ukflood"
|
||||
if enableDEalerts and not enableGBalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
@@ -207,7 +207,7 @@ if radio_detection_enabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled or read_news_enabled:
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
@@ -439,8 +439,35 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
else:
|
||||
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def handleFavoritNode(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
if aor:
|
||||
interface.getNode(myNodeNumber).addFavorite(nodeID)
|
||||
logger.info(f"System: Added {nodeID} to favorites")
|
||||
else:
|
||||
interface.getNode(myNodeNumber).removeFavorite(nodeID)
|
||||
logger.info(f"System: Removed {nodeID} from favorites")
|
||||
|
||||
def getFavoritNodes(nodeInt=1):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
favList = []
|
||||
for node in interface.getNode(myNodeNumber).favorites:
|
||||
favList.append(node)
|
||||
return favList
|
||||
|
||||
def handleSentinelIgnore(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
if aor:
|
||||
sentryIgnoreList.append(str(nodeID))
|
||||
logger.info(f"System: Added {nodeID} to sentry ignore list")
|
||||
else:
|
||||
sentryIgnoreList.remove(str(nodeID))
|
||||
logger.info(f"System: Removed {nodeID} from sentry ignore list")
|
||||
|
||||
|
||||
def messageChunker(message):
|
||||
message_list = []
|
||||
if len(message) > MESSAGE_CHUNK_SIZE:
|
||||
@@ -460,9 +487,9 @@ def messageChunker(message):
|
||||
sentence = ''
|
||||
for char in part:
|
||||
sentence += char
|
||||
if char in '.!?':
|
||||
sentences.append(sentence.strip())
|
||||
sentence = ''
|
||||
# if char in '.!?':
|
||||
# sentences.append(sentence.strip())
|
||||
# sentence = ''
|
||||
if sentence:
|
||||
sentences.append(sentence.strip())
|
||||
|
||||
@@ -496,8 +523,12 @@ def messageChunker(message):
|
||||
final_message_list = []
|
||||
for chunk in message_list:
|
||||
while len(chunk) > MESSAGE_CHUNK_SIZE:
|
||||
final_message_list.append(chunk[:MESSAGE_CHUNK_SIZE])
|
||||
chunk = chunk[MESSAGE_CHUNK_SIZE:]
|
||||
# Find the last space within the chunk size limit
|
||||
split_index = chunk.rfind(' ', 0, MESSAGE_CHUNK_SIZE)
|
||||
if split_index == -1:
|
||||
split_index = MESSAGE_CHUNK_SIZE
|
||||
final_message_list.append(chunk[:split_index])
|
||||
chunk = chunk[split_index:].strip()
|
||||
if chunk:
|
||||
final_message_list.append(chunk)
|
||||
|
||||
|
||||
@@ -7,17 +7,26 @@
|
||||
import os
|
||||
import http.server
|
||||
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
|
||||
# Set the port for the server
|
||||
PORT = 8420
|
||||
|
||||
# set webRoot index.html location
|
||||
webRoot = "etc/www"
|
||||
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
|
||||
SSL = False
|
||||
|
||||
# Set to True to enable logging sdtout
|
||||
webServerLogs = False
|
||||
|
||||
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
|
||||
SSL = False
|
||||
# Determine the directory where this script is located.
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# Go up one level from the modules directory to the project root.
|
||||
project_root = os.path.abspath(os.path.join(script_dir, ".."))
|
||||
|
||||
# Build the absolute path to the webRoot folder; to where index.html is located.
|
||||
webRoot = os.path.join(project_root, "etc", "www")
|
||||
|
||||
if SSL:
|
||||
import ssl
|
||||
@@ -43,7 +52,12 @@ if SSL:
|
||||
exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
|
||||
print(f"Serving reports at http://localhost:{PORT} Press ^C to quit.\n\n")
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
# Print out the URL using the IP address stored in server_ip
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
|
||||
if not webServerLogs:
|
||||
print("Server Logs are disabled")
|
||||
# Serve forever, that is until the user interrupts the process
|
||||
|
||||
10
pong_bot.py
10
pong_bot.py
@@ -25,8 +25,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
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,
|
||||
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"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),
|
||||
@@ -56,6 +55,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_cmd(message, message_from_id, deviceID):
|
||||
# why CMD? its just a command list. a terminal would normally use "Help"
|
||||
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
if "?" in message and isDM:
|
||||
|
||||
53
script/send-environment-metrics.py
Normal file
53
script/send-environment-metrics.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# file name: send-environment-metrics.py
|
||||
# https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/send-environment-metrics.py
|
||||
|
||||
from meshtastic.protobuf import portnums_pb2, telemetry_pb2
|
||||
from meshtastic import BROADCAST_ADDR
|
||||
import time
|
||||
|
||||
# For connection over serial
|
||||
# import meshtastic.serial_interface
|
||||
# interface = meshtastic.serial_interface.SerialInterface()
|
||||
|
||||
# For connection over TCP
|
||||
import meshtastic.tcp_interface
|
||||
interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=False)
|
||||
|
||||
# Create a telemetry data object
|
||||
telemetry_data = telemetry_pb2.Telemetry()
|
||||
telemetry_data.time = int(time.time())
|
||||
telemetry_data.local_stats.upTime = 0
|
||||
telemetry_data.environment_metrics.temperature = 0
|
||||
# telemetry_data.environment_metrics.voltage = 0
|
||||
# telemetry_data.environment_metrics.current = 0
|
||||
# telemetry_data.environment_metrics.relative_humidity = 0
|
||||
# telemetry_data.environment_metrics.barometric_pressure = 0
|
||||
# telemetry_data.environment_metrics.gas_resistance = 0
|
||||
# telemetry_data.environment_metrics.iaq = 0
|
||||
# telemetry_data.environment_metrics.distance = 0
|
||||
# telemetry_data.environment_metrics.lux = 0
|
||||
# telemetry_data.environment_metrics.white_lux = 0
|
||||
# telemetry_data.environment_metrics.ir_lux = 0
|
||||
# telemetry_data.environment_metrics.uv_lux = 0
|
||||
# telemetry_data.environment_metrics.wind_direction = 0
|
||||
# telemetry_data.environment_metrics.wind_speed = 0
|
||||
# telemetry_data.environment_metrics.wind_gust = 0
|
||||
# telemetry_data.environment_metrics.wind_lull = 0
|
||||
# telemetry_data.environment_metrics.weight = 0
|
||||
|
||||
# Read the uptime
|
||||
with open('/proc/uptime', 'r') as uptime:
|
||||
telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
|
||||
|
||||
# Read the CPU temperature
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
|
||||
telemetry_data.environment_metrics.temperature = int(cpu_temp.read()) / 1000
|
||||
|
||||
interface.sendData(
|
||||
telemetry_data,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
|
||||
wantResponse=False,
|
||||
)
|
||||
|
||||
interface.close()
|
||||
Reference in New Issue
Block a user