forked from iarv/meshing-around
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9ada91b4 | ||
|
|
28f06f0a21 | ||
|
|
267fe392e3 | ||
|
|
6c1f7940ca | ||
|
|
2fc9281394 | ||
|
|
b5bd1008c2 | ||
|
|
ee1db5b7be | ||
|
|
7395b96337 | ||
|
|
f3c6f77b23 | ||
|
|
f6e04a42a0 | ||
|
|
3fcd588d02 | ||
|
|
e1b47484f2 | ||
|
|
14798cb992 | ||
|
|
41c8f0044b | ||
|
|
45eefb24d8 | ||
|
|
410d32947c | ||
|
|
748652ac62 | ||
|
|
d715cb6b4d | ||
|
|
1895a365ae | ||
|
|
cc58a38165 | ||
|
|
a8ccb05d56 | ||
|
|
a90a533a30 | ||
|
|
57a4e5d68c | ||
|
|
7c99b684ad | ||
|
|
b957c89d70 | ||
|
|
9b986dd57a | ||
|
|
9e348332e5 | ||
|
|
0cfe759ef6 | ||
|
|
e95902ef98 | ||
|
|
c7df4d88d1 | ||
|
|
6d01c5a986 | ||
|
|
3f882dcfcd | ||
|
|
b146fd6f64 | ||
|
|
8709e5aed5 | ||
|
|
caf8a2708b | ||
|
|
9b4200c198 | ||
|
|
097cae6e94 | ||
|
|
0a260b28b6 | ||
|
|
3f5c6f2e9a | ||
|
|
8a4f7a904a | ||
|
|
0bc3d392cf | ||
|
|
5eaef8b5b8 | ||
|
|
3a0007771d | ||
|
|
67ba2b1fb5 | ||
|
|
f2e7a9aa5c | ||
|
|
9d22270dde | ||
|
|
409d07436e | ||
|
|
5ab0001f2b | ||
|
|
5e34537af7 | ||
|
|
1764bdf4f3 | ||
|
|
2290f07351 | ||
|
|
ee01051cf7 | ||
|
|
de50a52fa6 | ||
|
|
8eabfaa9c4 | ||
|
|
ca7114b058 | ||
|
|
8b94dc8111 | ||
|
|
5b26aabb00 | ||
|
|
67b3c67348 | ||
|
|
860cceec59 | ||
|
|
53a0535e55 | ||
|
|
621f4ad916 | ||
|
|
118857ec15 | ||
|
|
1be13be92a | ||
|
|
895fc3fd37 | ||
|
|
0e0bda60ad | ||
|
|
903767f4b3 | ||
|
|
f54d362ea0 | ||
|
|
60bb68c6b5 | ||
|
|
feb9a1d9b3 | ||
|
|
d055c35c96 | ||
|
|
27820daaf4 | ||
|
|
56e8e1c0d5 | ||
|
|
4545b8f4a4 | ||
|
|
6ed48d49ce | ||
|
|
a3a54b081d | ||
|
|
ab420af63e | ||
|
|
a55c61c47d | ||
|
|
7236f47eb7 | ||
|
|
05e11ae5f8 | ||
|
|
f8ffcc19b1 | ||
|
|
ea20eec604 | ||
|
|
d1204d2c26 | ||
|
|
654d8b3ff7 | ||
|
|
3bf12d62b5 | ||
|
|
0ec8613d27 | ||
|
|
10dd413ae7 | ||
|
|
09ac7525b3 | ||
|
|
aac497dfa0 |
216
README.md
216
README.md
@@ -38,6 +38,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
|
||||
|
||||
### CheckList / Check In Out
|
||||
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Usefull for accountability of people, assets. Radio-Net, FEMA, Trailhead.
|
||||
@@ -71,23 +72,97 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
## 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), 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
|
||||
|
||||
### Quick Setup
|
||||
#### Clone the Repository
|
||||
If you dont have git you will need it `sudo apt-get install git`
|
||||
```sh
|
||||
git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
The code is under active development, so make sure to pull the latest changes regularly!
|
||||
|
||||
#### Quick setup
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
#### Docker Installation
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `test` | used to test the limits of data transfer `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `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`| |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) | |
|
||||
| `valert` | Returns USGS Volcano Data | |
|
||||
| `wx` | Return local weather forecast, NOAA or Open Meteo (which also has `wxc` for metric and imperial) | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `mwx` | Return the NOAA Coastal Marine Forcast data | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
|
||||
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
|
||||
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
|
||||
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
|
||||
| `sms:` | Send sms-email to multiple address on file | |
|
||||
| `setemail`| Sets the email for easy communciations | |
|
||||
| `setsms` | Adds the SMS-Email for quick communications | |
|
||||
| `clearsms` | Clears all SMS-Emails on file for node | |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
|
||||
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `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 | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
|
||||
| `hangman` | Plays the classic word guess game | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
## Other Install Options
|
||||
|
||||
### Docker Installation - handy for windows
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
#### Manual Install
|
||||
### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -147,7 +222,12 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
|
||||
|
||||
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
# Find the correct costal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
|
||||
castalForecastDays = 3 # number of data points to return, default is 3
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
@@ -181,6 +261,8 @@ SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
highFlyingAlert = True # HighFlying Node alert
|
||||
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
@@ -212,20 +294,21 @@ alert_interface = 1
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages.
|
||||
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# 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
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
@@ -236,6 +319,22 @@ enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
wxAlertBroadcastEnabled = True
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
#### USGS River flow data and Volcano alerts
|
||||
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
|
||||
|
||||
Volcano Alerts use lat/long to determine ~1000km radius
|
||||
```ini
|
||||
[location]
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverListDefault = 14144700
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
@@ -334,9 +433,20 @@ In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = True
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
```
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -362,81 +472,6 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
|
||||
~~There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two~~
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `test` | used to test the limits of data transfer `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `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`| |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) | |
|
||||
| `valert` | Returns USGS Volcano Data | |
|
||||
| `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 | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
|
||||
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
|
||||
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
|
||||
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
|
||||
| `sms:` | Send sms-email to multiple address on file | |
|
||||
| `setemail`| Sets the email for easy communciations | |
|
||||
| `setsms` | Adds the SMS-Email for quick communications | |
|
||||
| `clearsms` | Clears all SMS-Emails on file for node | |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
|
||||
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `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 | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
|
||||
| `hangman` | Plays the classic word guess game | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
# Recognition
|
||||
|
||||
I used ideas and snippets from other responder bots and want to call them out!
|
||||
@@ -466,6 +501,7 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **dj505**: trying it on windows!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **trs2982**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ ollama = False
|
||||
# ollamaModel = llama3.1
|
||||
# server instance to use (defaults to local machine install)
|
||||
ollamaHostName = http://localhost:11434
|
||||
# Produce LLM replies to messages that aren't commands?
|
||||
# If False, the LLM only replies to the "ask:" and "askai" commands.
|
||||
llmReplyToNonCommands = True
|
||||
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
@@ -84,6 +87,9 @@ sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
#Do not retry enabling interface if it fails, just exit to let OS restart the bot
|
||||
dont_retry_disconnect = False
|
||||
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = False
|
||||
@@ -102,7 +108,16 @@ SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
sentryIgnoreList =
|
||||
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
# Altitude in meters to trigger the alert
|
||||
highFlyingAlertAltitude = 2000
|
||||
# Channel to send Alert when the high flying node is detected
|
||||
highFlyingAlertChannel = 2
|
||||
# list of nodes numbers to ignore high flying alert ex: 2813308004,4258675309
|
||||
highFlyingIgnoreList =
|
||||
|
||||
[bbs]
|
||||
enabled = True
|
||||
@@ -135,11 +150,24 @@ 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
|
||||
# NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
coastalEnabled = False
|
||||
# Find the correct costal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# pz = Puget Sound, ph = Honolulu HI, gm = Florida Keys, pk = Alaska
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
# myCoastalZone is the .txt file with the forecast data
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt
|
||||
# number of data points to return, default is 3
|
||||
coastalForecastDays = 3
|
||||
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverListDefault =
|
||||
|
||||
# NOAA EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreEASenable = False
|
||||
ignoreEASwords = test,advisory
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
# Add extra location to the weather alert
|
||||
@@ -147,20 +175,22 @@ enableExtraLocationWx = False
|
||||
|
||||
# Goverment Alert Broadcast defaults to FEMA IPAWS
|
||||
eAlertBroadcastEnabled = False
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
# Goverment Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
|
||||
# FEMA Alert Broadcast Settings
|
||||
# Enable Ignore any headline that includes followig word list
|
||||
# Enable Ignore, headline that includes following word list
|
||||
ignoreFEMAenable = True
|
||||
ignoreFEMAwords = test,exercise
|
||||
# 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
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreUSGSEnable = False
|
||||
ignoreUSGSWords = test,advisory
|
||||
|
||||
# Use DE Alert Broadcast Data
|
||||
enableDEalerts = False
|
||||
@@ -184,7 +214,7 @@ reverse_in_out = False
|
||||
# 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."
|
||||
qrz_hello_string = "MeshBot says Hello! DM for more info."
|
||||
# Training mode will not send the hello message to new nodes
|
||||
training = True
|
||||
|
||||
@@ -200,6 +230,17 @@ repeater_channels =
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
|
||||
[radioMon]
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
@@ -266,15 +307,17 @@ hangman = True
|
||||
hamtest = True
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 1.2
|
||||
# delay in seconds for splits in messages to avoid message collision
|
||||
splitDelay = 0.0
|
||||
# delay in seconds for response to avoid message collision /throttling
|
||||
responseDelay = 2.2
|
||||
# delay in seconds for splits in messages to avoid message collision /throttling
|
||||
splitDelay = 2.5
|
||||
# message chunk size for sending at high success rate, chunkr allows exceeding by 3 characters
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
|
||||
# Max limit buffer for radio testing
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
|
||||
|
||||
|
||||
@@ -350,7 +350,8 @@ def get_database_info():
|
||||
os.path.join(base_dir, 'mmind_hs.pkl'),
|
||||
os.path.join(base_dir, 'golfsim_hs.pkl'),
|
||||
os.path.join(base_dir, 'bbsdb.pkl'),
|
||||
os.path.join(base_dir, 'bbsdm.pkl')]
|
||||
os.path.join(base_dir, 'bbsdm.pkl'),
|
||||
os.path.join(base_dir, 'qrz.db')]
|
||||
|
||||
for file in databaseFiles:
|
||||
try:
|
||||
@@ -371,6 +372,16 @@ def get_database_info():
|
||||
bbsdb = pickle.load(f)
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
elif 'qrz.db' in file:
|
||||
# open the qrz.db sqllite file
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM qrz")
|
||||
qrz_db = cursor.fetchall()
|
||||
# convert to a list of strings
|
||||
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
@@ -425,7 +436,8 @@ def get_database_info():
|
||||
'golfsim_score': golfsim_score,
|
||||
'banList': banList,
|
||||
'adminList': adminList,
|
||||
'sentryIgnoreList': sentryIgnoreList
|
||||
'sentryIgnoreList': sentryIgnoreList,
|
||||
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
|
||||
}
|
||||
|
||||
def generate_main_html(log_data, system_info):
|
||||
@@ -913,6 +925,11 @@ def generate_database_html(database_info):
|
||||
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
|
||||
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
|
||||
</table>
|
||||
<h1>QRZ Database</h1>
|
||||
<p>QRZ Database holds heard nodeID and Shortname</p>
|
||||
<table>
|
||||
<tr><td>${qrz_db}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@@ -359,7 +359,8 @@ def get_database_info():
|
||||
os.path.join(base_dir, 'mmind_hs.pkl'),
|
||||
os.path.join(base_dir, 'golfsim_hs.pkl'),
|
||||
os.path.join(base_dir, 'bbsdb.pkl'),
|
||||
os.path.join(base_dir, 'bbsdm.pkl')]
|
||||
os.path.join(base_dir, 'bbsdm.pkl'),
|
||||
os.path.join(base_dir, 'qrz.db')]
|
||||
|
||||
for file in databaseFiles:
|
||||
try:
|
||||
@@ -380,6 +381,16 @@ def get_database_info():
|
||||
bbsdb = pickle.load(f)
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
elif 'qrz.db' in file:
|
||||
#open the qrz.db sqllite file
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(file)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM qrz")
|
||||
qrz_db = cursor.fetchall()
|
||||
# convert to a list of strings
|
||||
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
@@ -434,7 +445,8 @@ def get_database_info():
|
||||
'golfsim_score': golfsim_score,
|
||||
'banList': banList,
|
||||
'adminList': adminList,
|
||||
'sentryIgnoreList': sentryIgnoreList
|
||||
'sentryIgnoreList': sentryIgnoreList,
|
||||
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
|
||||
}
|
||||
|
||||
def generate_main_html(log_data, system_info):
|
||||
@@ -1207,6 +1219,11 @@ def generate_database_html(database_info):
|
||||
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
|
||||
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
|
||||
</table>
|
||||
<h1>QRZ Database</h1>
|
||||
<p>QRZ Database holds heard nodeID and Shortname</p>
|
||||
<table>
|
||||
<tr><td>${qrz_db}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
34
install.sh
34
install.sh
@@ -4,6 +4,7 @@
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "########################\n"
|
||||
@@ -193,17 +194,19 @@ if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" |
|
||||
sudo usermod -a -G meshbot meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added meshbot to dialout, tty, and bluetooth groups"
|
||||
|
||||
sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
else
|
||||
whoami=$(whoami)
|
||||
fi
|
||||
# set basic permissions for the bot user
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added user $whoami to dialout, tty, and bluetooth groups"
|
||||
|
||||
sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
@@ -276,6 +279,8 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
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
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> 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
|
||||
@@ -305,6 +310,14 @@ else
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
# check if the cron job already exists
|
||||
if ! crontab -l | grep -q "$chronjob"; then
|
||||
# add the cron job to run the report_generator5.py script
|
||||
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
|
||||
printf "\nAdded cron job to run report_generator5.py\n"
|
||||
else
|
||||
printf "\nCron job already exists, skipping\n"
|
||||
fi
|
||||
printf "Reference following commands:\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
|
||||
@@ -328,7 +341,6 @@ exit 0
|
||||
# sudo systemctl stop mesh_bot_reporting
|
||||
# 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
|
||||
@@ -344,5 +356,5 @@ exit 0
|
||||
|
||||
|
||||
# after install shenannigans
|
||||
# add 'bee = True' to config.ini General section. You will likley want to clean the txt up a bit
|
||||
# wget https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt -O bee.txt
|
||||
# add 'bee = True' to config.ini General section.
|
||||
# wget https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a -O bee.txt
|
||||
|
||||
@@ -24,4 +24,18 @@ log_backup_count = 32
|
||||
## Web Reporting WebServer
|
||||
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
|
||||
|
||||
find it at. http://localhost:8420
|
||||
find it at. http://localhost:8420
|
||||
|
||||
If you have linux-native running and errors such as..
|
||||
```bash
|
||||
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
|
||||
self.socket.bind(self.server_address)
|
||||
```
|
||||
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
|
||||
|
||||
```python
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
```
|
||||
|
||||
110
mesh_bot.py
110
mesh_bot.py
@@ -20,7 +20,6 @@ restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
global cmdHistory
|
||||
@@ -68,6 +67,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
|
||||
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
|
||||
"motd": lambda: handle_motd(message, message_from_id, isDM),
|
||||
"mwx": lambda: handle_mwx(message_from_id, deviceID, channel_number),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
@@ -751,6 +751,12 @@ def handle_riverFlow(message, message_from_id, deviceID):
|
||||
msg = get_flood_noaa(location[0], location[1], userRiver)
|
||||
return msg
|
||||
|
||||
def handle_mwx(message_from_id, deviceID, cmd):
|
||||
# NOAA Coastal and Marine Weather
|
||||
if myCoastalZone is None:
|
||||
logger.warning("System: Coastal Zone not set, please set in config.ini")
|
||||
return NO_ALERTS
|
||||
return get_nws_marine(zone=myCoastalZone, days=castalForecastDays)
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
@@ -805,7 +811,7 @@ def handle_bbspost(message, message_from_id, deviceID):
|
||||
toNode = int(toNode.strip("!"),16)
|
||||
except ValueError as e:
|
||||
toNode = 0
|
||||
elif toNode.isalpha() or not toNode.isnumeric():
|
||||
elif toNode.isalpha() or not toNode.isnumeric() or len(toNode) < 5:
|
||||
# try short name
|
||||
toNode = get_num_from_short_name(toNode, deviceID)
|
||||
|
||||
@@ -904,7 +910,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
|
||||
prettyTime = getPrettyTime(cmdTime)
|
||||
|
||||
# history display output
|
||||
if nodeid in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
if str(nodeid) in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(cmdHistory[i]['nodeID'], 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
elif cmdHistory[i]['nodeID'] == nodeid and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(nodeid, 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
@@ -1108,16 +1114,15 @@ def onReceive(packet, interface):
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
@@ -1194,7 +1199,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if DEBUGhops:
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
logger.debug(f"System: Packet HopDebugger: No hop count found in PACKET {packet} END PACKET")
|
||||
@@ -1226,7 +1231,7 @@ def onReceive(packet, interface):
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
# log the message to stdout
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
@@ -1244,7 +1249,7 @@ def onReceive(packet, interface):
|
||||
playingGame = False
|
||||
|
||||
if not playingGame:
|
||||
if llm_enabled:
|
||||
if llm_enabled and llmReplyToNonCommands:
|
||||
# respond with LLM
|
||||
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
@@ -1273,7 +1278,8 @@ def onReceive(packet, interface):
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# log the message to the message log
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
@@ -1399,6 +1405,8 @@ async def start_rx():
|
||||
logger.debug("System: Location Telemetry Enabled using NOAA API")
|
||||
if dad_jokes_enabled:
|
||||
logger.debug("System: Dad Jokes Enabled!")
|
||||
if coastalEnabled:
|
||||
logger.debug("System: Coastal Forcast and Tide Enabled!")
|
||||
if games_enabled:
|
||||
logger.debug("System: Games Enabled!")
|
||||
if wikipedia_enabled:
|
||||
@@ -1407,6 +1415,8 @@ async def start_rx():
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
@@ -1426,7 +1436,10 @@ async def start_rx():
|
||||
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}")
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh} for FIPS codes {myStateFIPSList}")
|
||||
# check if the FIPS codes are set
|
||||
if myStateFIPSList == ['']:
|
||||
logger.warning(f"System: No FIPS codes set for iPAWS Alerts")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
@@ -1445,11 +1458,51 @@ async def start_rx():
|
||||
else:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
# basic scheduler
|
||||
if schedulerValue != '':
|
||||
logger.debug(f"System: Starting the broadcast scheduler from config.ini")
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
# Send a message every day at the time set in schedulerTime
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
# Send a message every day at the time set in schedulerInterval
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Monday at the time set in schedulerTime
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Tuesday at the time set in schedulerTime
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Wednesday at the time set in schedulerTime
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Thursday at the time set in schedulerTime
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Friday at the time set in schedulerTime
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Saturday at the time set in schedulerTime
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Sunday at the time set in schedulerTime
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
# Send a message every hour at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
# Send a message every minute at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
logger.debug(f"System: Starting the broadcast scheduler")
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
@@ -1483,8 +1536,6 @@ async def start_rx():
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
|
||||
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
@@ -1509,10 +1560,11 @@ async def main():
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
except SystemExit:
|
||||
pass
|
||||
# EOF
|
||||
|
||||
@@ -39,4 +39,16 @@ To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy
|
||||
```
|
||||
|
||||
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.
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
|
||||
|
||||
### Running a Shell command
|
||||
|
||||
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
|
||||
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"switchON": lambda: call_external_script(message)
|
||||
```
|
||||
This would call the default script located in script/runShell.sh and return its output.
|
||||
@@ -31,7 +31,7 @@ def read_file(file_monitor_file_path, random_line_only=False):
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path, read_news_enabled)
|
||||
return read_file(news_file_path, news_random_line_only)
|
||||
|
||||
|
||||
def write_news(content, append=False):
|
||||
|
||||
@@ -164,7 +164,7 @@ class PlayerVP:
|
||||
return self.show_hand()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
|
||||
|
||||
# Method for scoring hand, calculating winnings, and outputting message
|
||||
@@ -390,7 +390,7 @@ def playVideoPoker(nodeID, message):
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
drawCount += 1
|
||||
@@ -403,7 +403,7 @@ def playVideoPoker(nodeID, message):
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
|
||||
@@ -146,6 +146,12 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
googleResults = []
|
||||
if not location_name:
|
||||
location_name = "no location provided "
|
||||
|
||||
# remove askai: and ask: from the input
|
||||
for trap in trap_list_llm:
|
||||
if input.lower().startswith(trap):
|
||||
input = input[len(trap):].strip()
|
||||
break
|
||||
|
||||
# add the naughty list here to stop the function before we continue
|
||||
# add a list of allowed nodes only to use the function
|
||||
|
||||
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow","valert")
|
||||
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -397,6 +397,12 @@ def alertBrodcastNOAA():
|
||||
elif currentAlert == NO_ALERTS:
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
if ignoreEASenable:
|
||||
# check if the alert is in the ignoreEAS list
|
||||
for word in ignoreEASwords:
|
||||
if word.lower() in currentAlert[0].lower():
|
||||
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
@@ -447,7 +453,7 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = alerts.split("\n***\n")[:numWxAlerts]
|
||||
|
||||
if alerts == "" or alerts == ['']:
|
||||
return ERROR_FETCHING_DATA
|
||||
return NO_ALERTS
|
||||
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
@@ -461,12 +467,11 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# get the latest IPAWS alert from FEMA
|
||||
alert = ''
|
||||
alerts = []
|
||||
linked_data = ''
|
||||
|
||||
# set the API URL for IPAWS
|
||||
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
|
||||
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
|
||||
if ipawsPIN != "000000":
|
||||
alert_url += "?pin=" + ipawsPIN
|
||||
|
||||
# get the alerts from FEMA
|
||||
try:
|
||||
@@ -484,23 +489,49 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# extract alerts from main feed
|
||||
for entry in alertxml.getElementsByTagName("entry"):
|
||||
link = entry.getElementsByTagName("link")[0].getAttribute("href")
|
||||
|
||||
## state FIPS
|
||||
## This logic is being added to reduce load on FEMA server.
|
||||
stateFips = None
|
||||
for cat in entry.getElementsByTagName("category"):
|
||||
if cat.getAttribute("label") == "statefips":
|
||||
stateFips = cat.getAttribute("term")
|
||||
break
|
||||
|
||||
if stateFips is None:
|
||||
# no stateFIPS found — skip
|
||||
continue
|
||||
|
||||
# check if it matches your list
|
||||
if stateFips not in myStateFIPSList:
|
||||
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {myStateFIPSList}")
|
||||
continue # skip to next entry
|
||||
|
||||
try:
|
||||
#pin check
|
||||
if ipawsPIN != "000000":
|
||||
link += "?pin=" + ipawsPIN
|
||||
# get the linked alert data from FEMA
|
||||
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
|
||||
if not linked_data.ok:
|
||||
if not linked_data.ok or not linked_data.text.strip():
|
||||
# if the linked data is not ok, skip this alert
|
||||
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
|
||||
continue
|
||||
else:
|
||||
linked_xml = xml.dom.minidom.parseString(linked_data.text)
|
||||
# this alert is a full CAP alert
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
|
||||
continue
|
||||
|
||||
# this alert is a full CAP alert
|
||||
linked_xml = xml.dom.minidom.parseString(linked_data.text)
|
||||
except xml.parsers.expat.ExpatError:
|
||||
logger.warning(f"System: iPAWS Error parsing XML from {link}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"System: iPAWS Error processing alert data from {link}: {e}")
|
||||
continue
|
||||
|
||||
for info in linked_xml.getElementsByTagName("info"):
|
||||
# only get en-US language alerts (alternative is es-US)
|
||||
language_nodes = info.getElementsByTagName("language")
|
||||
if not any(node.firstChild and node.firstChild.nodeValue.strip() == "en-US" for node in language_nodes):
|
||||
continue # skip if not en-US
|
||||
# extract values from XML
|
||||
sameVal = "NONE"
|
||||
geocode_value = "NONE"
|
||||
@@ -518,27 +549,31 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
area_table = info.getElementsByTagName("area")[0]
|
||||
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
|
||||
|
||||
geocode_table = area_table.getElementsByTagName("geocode")[0]
|
||||
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: iPAWS Error extracting alert data: {link}")
|
||||
#print(f"DEBUG: {info.toprettyxml()}")
|
||||
continue
|
||||
|
||||
# check if the alert is for the current location, if wanted keep alert
|
||||
if (sameVal in mySAME) or (geocode_value in mySAME):
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
# ignore the FEMA test alerts
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Ignoring FEMA Alert: {headline} containing {word} at {areaDesc}")
|
||||
continue
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
# add to alert list
|
||||
alerts.append({
|
||||
'alertType': alertType,
|
||||
'alertCode': alertCode,
|
||||
@@ -548,10 +583,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
'geocode_value': geocode_value,
|
||||
'description': description
|
||||
})
|
||||
# else:
|
||||
# # these are discarded some day but logged for debugging currently
|
||||
# logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
|
||||
|
||||
else:
|
||||
logger.debug(f"System: iPAWS Alert not in SAME List: {sameVal} or {geocode_value} for {headline} at {areaDesc}")
|
||||
continue
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
for alertItem in alerts[:numWxAlerts]:
|
||||
@@ -626,26 +661,36 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
try:
|
||||
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
|
||||
if not volcano_data.ok:
|
||||
logger.warning("System: USGS fetching volcano alerts from USGS")
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: USGS fetching volcano alerts from USGS")
|
||||
logger.warning("System: Issue with fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
volcano_json = volcano_data.json()
|
||||
# extract alerts from main feed
|
||||
for alert in volcano_json:
|
||||
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
|
||||
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
|
||||
volcano_name = alert['volcano_name_appended']
|
||||
alert_level = alert['alert_level']
|
||||
color_code = alert['color_code']
|
||||
cap_severity = alert['cap_severity']
|
||||
synopsis = alert['synopsis']
|
||||
# format Alert
|
||||
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
|
||||
else:
|
||||
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
|
||||
continue
|
||||
if volcano_json and isinstance(volcano_json, list):
|
||||
for alert in volcano_json:
|
||||
# check ignore list
|
||||
if ignoreUSGSEnable:
|
||||
for word in ignoreUSGSwords:
|
||||
if word.lower() in alert['volcano_name_appended'].lower():
|
||||
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
|
||||
continue
|
||||
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
|
||||
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
|
||||
volcano_name = alert['volcano_name_appended']
|
||||
alert_level = alert['alert_level']
|
||||
color_code = alert['color_code']
|
||||
cap_severity = alert['cap_severity']
|
||||
synopsis = alert['synopsis']
|
||||
# format Alert
|
||||
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
|
||||
else:
|
||||
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
|
||||
continue
|
||||
else:
|
||||
logger.debug("Location:Error fetching volcano data from USGS")
|
||||
return NO_ALERTS
|
||||
if alerts == "":
|
||||
return NO_ALERTS
|
||||
# trim off last newline
|
||||
@@ -655,4 +700,60 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
return alerts
|
||||
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forcast from NWS coastal products
|
||||
try:
|
||||
marine_pzz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pzz_data.ok:
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
marine_pzz_data = marine_pzz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pzz_data.startswith("Expires:"):
|
||||
expires = marine_pzz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
marine_pzz_lines = marine_pzz_data.split("\n")
|
||||
marine_pzz_report = ""
|
||||
day_blocks = []
|
||||
current_block = ""
|
||||
in_forecast = False
|
||||
|
||||
for line in marine_pzz_lines:
|
||||
if line.startswith(".") and "..." in line:
|
||||
in_forecast = True
|
||||
if current_block:
|
||||
day_blocks.append(current_block.strip())
|
||||
current_block = ""
|
||||
current_block += line.strip() + " "
|
||||
elif in_forecast and line.strip() != "":
|
||||
current_block += line.strip() + " "
|
||||
if current_block:
|
||||
day_blocks.append(current_block.strip())
|
||||
|
||||
# Only keep up to pzzDays blocks
|
||||
for block in day_blocks[:days]:
|
||||
marine_pzz_report += block + "\n"
|
||||
|
||||
# remove last newline
|
||||
if marine_pzz_report.endswith("\n"):
|
||||
marine_pzz_report = marine_pzz_report[:-1]
|
||||
|
||||
# abbreviate the report
|
||||
marine_pzz_report = abbreviate_noaa(marine_pzz_report)
|
||||
if marine_pzz_report == "":
|
||||
return NO_DATA_NOGPS
|
||||
return marine_pzz_report
|
||||
|
||||
|
||||
@@ -83,18 +83,14 @@ if log_messages_to_file:
|
||||
|
||||
# Pretty Timestamp
|
||||
def getPrettyTime(seconds):
|
||||
# convert unix time to minutes, hours, or days, or years for simple display
|
||||
designator = "s"
|
||||
if seconds > 0:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "m"
|
||||
if seconds > 60:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "h"
|
||||
if seconds > 24:
|
||||
seconds = round(seconds / 24)
|
||||
designator = "d"
|
||||
if seconds > 365:
|
||||
seconds = round(seconds / 365)
|
||||
designator = "y"
|
||||
return str(seconds) + designator
|
||||
# convert unix time to minutes, hours, days, or years for simple display
|
||||
if seconds < 60:
|
||||
return f"{int(seconds)}s"
|
||||
elif seconds < 3600:
|
||||
return f"{int(round(seconds / 60))}m"
|
||||
elif seconds < 86400:
|
||||
return f"{int(round(seconds / 3600))}h"
|
||||
elif seconds < 31536000:
|
||||
return f"{int(round(seconds / 86400))}d"
|
||||
else:
|
||||
return f"{int(round(seconds / 31536000))}y"
|
||||
@@ -21,8 +21,7 @@ ping_enabled = True # ping feature to respond to pings, ack's etc.
|
||||
sitrep_enabled = True # sitrep feature to respond to sitreps
|
||||
lastHamLibAlert = 0 # last alert from hamlib
|
||||
lastFileAlert = 0 # last alert from file monitor
|
||||
max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 4 # default retry count for interfaces
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
@@ -222,6 +221,8 @@ try:
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True)
|
||||
dont_retry_disconnect = config['general'].getboolean('dont_retry_disconnect', False) # default False, retry on disconnect
|
||||
# emergency response
|
||||
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
|
||||
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
|
||||
@@ -235,6 +236,10 @@ try:
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
|
||||
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
|
||||
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
|
||||
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -246,24 +251,32 @@ try:
|
||||
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
|
||||
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
|
||||
coastalEnabled = config['location'].getboolean('coastalEnabled', False) # default False
|
||||
myCoastalZone = config['location'].get('myCoastalZone', None) # default None
|
||||
coastalForecastDays = config['location'].getint('coastalForecastDays', 3) # default 3 days
|
||||
|
||||
# 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
|
||||
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
|
||||
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
|
||||
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
myStateFIPSList = config['location'].get('myFIPSList', '').split(',') # default empty
|
||||
mySAMEList = config['location'].get('mySAMEList', '').split(',') # default empty
|
||||
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
|
||||
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
|
||||
volcanoAlertBroadcastChannel = config['location'].get('volcanoAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
|
||||
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
@@ -281,7 +294,7 @@ try:
|
||||
# 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.')
|
||||
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
|
||||
train_qrz = config['qrz'].getboolean('training', True)
|
||||
|
||||
# E-Mail Settings
|
||||
@@ -307,6 +320,12 @@ try:
|
||||
|
||||
# scheduler
|
||||
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
|
||||
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
|
||||
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
|
||||
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
|
||||
schedulerInterval = config['scheduler'].get('interval', '') # default empty
|
||||
schedulerTime = config['scheduler'].get('time', '') # default empty
|
||||
schedulerValue = config['scheduler'].get('value', '') # default empty
|
||||
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
@@ -320,7 +339,7 @@ try:
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
|
||||
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 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
|
||||
@@ -343,6 +362,7 @@ try:
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
@@ -18,6 +18,7 @@ help_message = "Bot CMD?:"
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
interface_retry_count = 3
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
@@ -69,12 +70,12 @@ else:
|
||||
if enableCmdHistory:
|
||||
trap_list = trap_list + ("history",)
|
||||
#help_message = help_message + ", history"
|
||||
|
||||
|
||||
# Location Configuration
|
||||
if location_enabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
|
||||
help_message = help_message + ", whereami, wx, wxc, rlist"
|
||||
trap_list = trap_list + trap_list_location
|
||||
help_message = help_message + ", whereami, wx"
|
||||
if enableGBalerts and not enableDEalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
logger.warning(f"System: GB Alerts not functional at this time need to find a source API")
|
||||
@@ -86,17 +87,25 @@ if location_enabled:
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
trap_list = trap_list + ("wxc",)
|
||||
help_message = help_message + ", wxc"
|
||||
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
|
||||
else:
|
||||
# NOAA only features
|
||||
help_message = help_message + ", wxa, tide"
|
||||
help_message = help_message + ", wxa"
|
||||
|
||||
# NOAA alerts needs location module
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
# limited subset, this should be done better but eh..
|
||||
trap_list = trap_list + ("wx", "wxc", "wxa", "wxalert", "ea", "ealert", "valert")
|
||||
trap_list = trap_list + ("wx", "wxa", "wxalert", "ea", "ealert", "valert")
|
||||
help_message = help_message + ", wxalert, ealert, valert"
|
||||
|
||||
# NOAA Coastal Waters Forecasts
|
||||
if coastalEnabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("mwx","tide",)
|
||||
help_message = help_message + ", mwx, tide"
|
||||
|
||||
# BBS Configuration
|
||||
if bbs_enabled:
|
||||
@@ -255,6 +264,7 @@ if ble_count > 1:
|
||||
logger.debug(f"System: Initializing Interfaces")
|
||||
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
|
||||
retry_int1 = retry_int2 = retry_int3 = retry_int4 = retry_int5 = retry_int6 = retry_int7 = retry_int8 = retry_int9 = False
|
||||
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = interface_retry_count
|
||||
for i in range(1, 10):
|
||||
interface_type = globals().get(f'interface{i}_type')
|
||||
if not interface_type or interface_type == 'none' or globals().get(f'interface{i}_enabled') == False:
|
||||
@@ -384,27 +394,27 @@ def get_node_list(nodeInt=1):
|
||||
|
||||
return node_list
|
||||
|
||||
def get_node_location(number, nodeInt=1, channel=0):
|
||||
def get_node_location(nodeID, nodeInt=1, channel=0):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the location of a node by its number from nodeDB on device
|
||||
# if no location data, return default location
|
||||
latitude = latitudeValue
|
||||
longitude = longitudeValue
|
||||
position = [latitudeValue,longitudeValue]
|
||||
lastheard = 0
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
if number == node['num']:
|
||||
if 'position' in node:
|
||||
if nodeID == node['num']:
|
||||
if 'position' in node and node['position'] is not {}:
|
||||
try:
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
logger.debug(f"System: location data for {nodeID} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error getting location data for {number}")
|
||||
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
logger.debug(f"System: No location data for {nodeID} use default location")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No location data for {number} using default location")
|
||||
logger.debug(f"System: No location data for {nodeID} using default location")
|
||||
# request location data
|
||||
# try:
|
||||
# logger.debug(f"System: Requesting location data for {number}")
|
||||
@@ -413,7 +423,7 @@ def get_node_location(number, nodeInt=1, channel=0):
|
||||
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No nodes found")
|
||||
logger.warning(f"System: Location for NodeID {nodeID} not found in nodeDb")
|
||||
return position
|
||||
|
||||
|
||||
@@ -540,6 +550,15 @@ def messageChunker(message):
|
||||
if current_chunk:
|
||||
message_list.append(current_chunk)
|
||||
|
||||
# Consolidate any adjacent messages that can fit in a single chunk.
|
||||
idx = 0
|
||||
while idx < len(message_list) - 1:
|
||||
if len(message_list[idx]) + len(message_list[idx+1]) < MESSAGE_CHUNK_SIZE:
|
||||
message_list[idx] += '\n' + message_list[idx+1]
|
||||
del message_list[idx+1]
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
# Ensure no chunk exceeds MESSAGE_CHUNK_SIZE
|
||||
final_message_list = []
|
||||
for chunk in message_list:
|
||||
@@ -779,7 +798,7 @@ def handleAlertBroadcast(deviceID=1):
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
if NO_ALERTS not in deAlert:
|
||||
if NO_ALERTS not in alertDe:
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(ukAlert, int(channel), 0, deviceID)
|
||||
@@ -804,7 +823,7 @@ def handleAlertBroadcast(deviceID=1):
|
||||
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue)
|
||||
if volcanoAlert and volcanoAlert != NO_ALERTS and volcanoAlert != ERROR_FETCHING_DATA:
|
||||
if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert:
|
||||
# check if the alert is different from the last one
|
||||
if volcanoAlert != priorVolcanoAlert:
|
||||
priorVolcanoAlert = volcanoAlert
|
||||
@@ -816,40 +835,9 @@ def handleAlertBroadcast(deviceID=1):
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
rxType = type(interface).__name__
|
||||
if rxType in ['SerialInterface', 'TCPInterface', 'BLEInterface']:
|
||||
identifier = interface.__dict__.get('devPath', interface.__dict__.get('hostname', 'BLE'))
|
||||
logger.critical(f"System: Lost Connection to Device {identifier}")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
if (rxType == 'SerialInterface' and globals().get(f'port{i}') in identifier) or \
|
||||
(rxType == 'TCPInterface' and globals().get(f'hostname{i}') in identifier) or \
|
||||
(rxType == 'BLEInterface' and globals().get(f'interface{i}_type') == 'ble'):
|
||||
globals()[f'retry_int{i}'] = True
|
||||
break
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
logger.debug(f"System: Closing Interface1")
|
||||
interface1.close()
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
logger.debug(f"System: Closing Interface{i}")
|
||||
globals()[f'interface{i}'].close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
save_bbsdb()
|
||||
save_bbsdm()
|
||||
logger.debug(f"System: BBS Messages Saved")
|
||||
logger.debug(f"System: Exiting")
|
||||
asyncLoop.stop()
|
||||
asyncLoop.close()
|
||||
exit (0)
|
||||
# Handle disconnection of the interface
|
||||
logger.warning(f"System: Abrupt Disconnection of Interface detected")
|
||||
interface.close()
|
||||
|
||||
# Telemetry Functions
|
||||
telemetryData = {}
|
||||
@@ -978,8 +966,15 @@ def consumeMetadata(packet, rxNode=0):
|
||||
|
||||
for key in keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# if altitude is over 2000 send a log and message for high-flying nodes and not in highfly_ignoreList
|
||||
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList:
|
||||
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} NodeID: {nodeID}")
|
||||
altFeet = round(position_data['altitude'] * 3.28084, 2)
|
||||
send_message(f"High Altitude {altFeet}ft ({position_data['altitude']}m) on Device:{rxNode} Node:{get_name_from_number(nodeID,'short',rxNode)}", highfly_channel, 0, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# Keep the positionMetadata dictionary at 5 records
|
||||
# Keep the positionMetadata dictionary at a maximum size of 20
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
@@ -1096,7 +1091,7 @@ async def handleFileWatcher():
|
||||
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
|
||||
if type(file_monitor_broadcastCh) is list:
|
||||
for ch in file_monitor_broadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
if antiSpam and int(ch) != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
@@ -1122,21 +1117,26 @@ async def handleFileWatcher():
|
||||
pass
|
||||
|
||||
async def retry_interface(nodeID):
|
||||
global max_retry_count
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
global max_retry_count1, max_retry_count2, max_retry_count3, max_retry_count4, max_retry_count5, max_retry_count6, max_retry_count7, max_retry_count8, max_retry_count9
|
||||
interface = globals()[f'interface{nodeID}']
|
||||
retry_int = globals()[f'retry_int{nodeID}']
|
||||
max_retry_count = globals()[f'max_retry_count{nodeID}']
|
||||
|
||||
if dont_retry_disconnect:
|
||||
logger.critical(f"System: dont_retry_disconnect is set, not retrying interface{nodeID}")
|
||||
exit_handler()
|
||||
|
||||
if interface is not None:
|
||||
retry_int = True
|
||||
max_retry_count -= 1
|
||||
globals()[f'retry_int{nodeID}'] = True
|
||||
globals()[f'max_retry_count{nodeID}'] -= 1
|
||||
logger.debug(f"System: Retrying interface{nodeID} {globals()[f'max_retry_count{nodeID}']} attempts left")
|
||||
try:
|
||||
interface.close()
|
||||
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing interface{nodeID}: {e}")
|
||||
|
||||
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
|
||||
if max_retry_count == 0:
|
||||
if globals()[f'max_retry_count{nodeID}'] == 0:
|
||||
logger.critical(f"System: Max retry count reached for interface{nodeID}")
|
||||
exit_handler()
|
||||
|
||||
@@ -1146,15 +1146,19 @@ async def retry_interface(nodeID):
|
||||
if retry_int:
|
||||
interface = None
|
||||
globals()[f'interface{nodeID}'] = None
|
||||
logger.debug(f"System: Retrying Interface{nodeID}")
|
||||
interface_type = globals()[f'interface{nodeID}_type']
|
||||
if interface_type == 'serial':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} Serial on port: {globals().get(f'port{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}'))
|
||||
elif interface_type == 'tcp':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} TCP on hostname: {globals().get(f'hostname{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}'))
|
||||
elif interface_type == 'ble':
|
||||
logger.debug(f"System: Retrying Interface{nodeID} BLE on mac: {globals().get(f'mac{nodeID}')}")
|
||||
globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}'))
|
||||
logger.debug(f"System: Interface{nodeID} Opened!")
|
||||
# reset the retry_int and retry_count
|
||||
globals()[f'max_retry_count{nodeID}'] = interface_retry_count
|
||||
globals()[f'retry_int{nodeID}'] = False
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
|
||||
@@ -1242,3 +1246,24 @@ async def watchdog():
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface{i}: {e}")
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
logger.debug(f"System: Closing Interface1")
|
||||
interface1.close()
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
logger.debug(f"System: Closing Interface{i}")
|
||||
globals()[f'interface{i}'].close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
save_bbsdb()
|
||||
save_bbsdm()
|
||||
logger.debug(f"System: BBS Messages Saved")
|
||||
logger.debug(f"System: Exiting")
|
||||
asyncLoop.stop()
|
||||
asyncLoop.close()
|
||||
exit (0)
|
||||
|
||||
@@ -40,8 +40,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
# Change the current working directory to webRoot
|
||||
os.chdir(webRoot)
|
||||
|
||||
# boot up simple HTTP server
|
||||
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
if SSL:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
@@ -51,12 +51,9 @@ if SSL:
|
||||
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
|
||||
exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
|
||||
# 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")
|
||||
print(f"Serving reports at https://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
else:
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
|
||||
if not webServerLogs:
|
||||
print("Server Logs are disabled")
|
||||
|
||||
34
pong_bot.py
34
pong_bot.py
@@ -217,15 +217,15 @@ def onReceive(packet, interface):
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
@@ -310,7 +310,7 @@ def onReceive(packet, interface):
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
# log the message to stdout
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
@@ -321,7 +321,8 @@ def onReceive(packet, interface):
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# log the message to the message log
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
@@ -443,10 +444,11 @@ async def main():
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
except SystemExit:
|
||||
pass
|
||||
# EOF
|
||||
|
||||
@@ -24,4 +24,21 @@ else
|
||||
fi
|
||||
|
||||
# print telemetry data rounded to 2 decimal places
|
||||
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
|
||||
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
|
||||
|
||||
# attempt check for updates
|
||||
if command -v git &> /dev/null
|
||||
then
|
||||
if [ -d ../.git ]; then
|
||||
# check for updates
|
||||
git fetch --quiet
|
||||
local_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$local_branch" != "HEAD" ] && git show-ref --verify --quiet "refs/remotes/origin/$local_branch"; then
|
||||
local_commit=$(git rev-parse "$local_branch")
|
||||
remote_commit=$(git rev-parse "origin/$local_branch")
|
||||
if [ "$local_commit" != "$remote_commit" ]; then
|
||||
echo "Bot Update Available!"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
46
update.sh
Normal file
46
update.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# MeshBot Update Script
|
||||
# Usage: bash update.sh or ./update.sh after making it executable with chmod +x update.sh
|
||||
|
||||
# Check if the mesh_bot.service or pong_bot.service
|
||||
if systemctl is-active --quiet mesh_bot.service; then
|
||||
echo "Stopping mesh_bot.service..."
|
||||
systemctl stop mesh_bot.service
|
||||
fi
|
||||
if systemctl is-active --quiet pong_bot.service; then
|
||||
echo "Stopping pong_bot.service..."
|
||||
systemctl stop pong_bot.service
|
||||
fi
|
||||
if systemctl is-active --quiet mesh_bot_reporting.service; then
|
||||
echo "Stopping mesh_bot_reporting.service..."
|
||||
systemctl stop mesh_bot_reporting.service
|
||||
fi
|
||||
if systemctl is-active --quiet mesh_bot_w3.service; then
|
||||
echo "Stopping mesh_bot_w3.service..."
|
||||
systemctl stop mesh_bot_w3.service
|
||||
fi
|
||||
|
||||
# Update the local repository
|
||||
echo "Updating local repository..."
|
||||
#git fetch --all
|
||||
#git reset --hard origin/main # Replace 'main' with your branch name if different
|
||||
git pull origin main --rebase # Fetch and rebase to keep local changes if any
|
||||
echo "Local repository updated."
|
||||
|
||||
# Install or update dependencies
|
||||
echo "Installing or updating dependencies..."
|
||||
pip install -r requirements.txt --upgrade
|
||||
|
||||
echo "Dependencies installed or updated."
|
||||
|
||||
# Restart the services
|
||||
echo "Restarting services..."
|
||||
systemctl start mesh_bot.service
|
||||
systemctl start pong_bot.service
|
||||
systemctl start mesh_bot_reporting.service
|
||||
systemctl start mesh_bot_w3.service
|
||||
echo "Services restarted."
|
||||
# Print completion message
|
||||
echo "Update completed successfully?"
|
||||
exit 0
|
||||
# End of script
|
||||
Reference in New Issue
Block a user