mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5ba56a656 | ||
|
|
50c3249edc | ||
|
|
80897f7a82 | ||
|
|
d311832a92 | ||
|
|
56af59345d | ||
|
|
c1adca7db0 | ||
|
|
4c7fe55b43 | ||
|
|
df6a1cfb66 | ||
|
|
9994446510 | ||
|
|
9272218815 | ||
|
|
388d862fc9 | ||
|
|
ac33f8a02b | ||
|
|
f04392a81c | ||
|
|
d0097c092b | ||
|
|
92ff166260 | ||
|
|
bfe0d219f9 | ||
|
|
85a2d90cff | ||
|
|
e15232875c | ||
|
|
d1a87f161b | ||
|
|
626ac59b4e | ||
|
|
835a9e5f89 | ||
|
|
3ae928dd66 | ||
|
|
3973406783 | ||
|
|
4fbdd42837 | ||
|
|
04378efdd8 | ||
|
|
0d19a40ed6 | ||
|
|
75ac3c974a | ||
|
|
7e0eb348ae | ||
|
|
af6ea2a512 | ||
|
|
6665ea7dcd | ||
|
|
3212661ee8 | ||
|
|
0675132171 | ||
|
|
fdb7897963 | ||
|
|
8ff7a0bf3c | ||
|
|
c210534543 | ||
|
|
ea7574a868 | ||
|
|
8f69c4d93c | ||
|
|
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 |
203
README.md
203
README.md
@@ -72,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 via DM only) | ✅ |
|
||||
| `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) via DM only | ✅ |
|
||||
| `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 only)
|
||||
| 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
|
||||
@@ -137,6 +211,7 @@ defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
@@ -148,7 +223,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
|
||||
coastalForecastDays = 3 # number of data points to return, default is 3
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
@@ -215,20 +295,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
|
||||
@@ -243,6 +324,20 @@ 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
|
||||
riverList = 14144700 # example Mouth of Columbia River
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
|
||||
@@ -253,12 +348,12 @@ repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### Ollama (LLM/AI) Settings
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = True # Ollama model to use (defaults to gemma2:2b)
|
||||
ollamaModel = gemma2 #ollamaModel = llama3.1
|
||||
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
|
||||
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
|
||||
```
|
||||
|
||||
@@ -266,6 +361,9 @@ Also see `llm.py` for changing the defaults of:
|
||||
|
||||
```ini
|
||||
# LLM System Variables
|
||||
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
|
||||
|
||||
# Used in the meshBotAI template (legacy)
|
||||
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
@@ -378,81 +476,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!
|
||||
|
||||
@@ -36,6 +36,8 @@ ignoreDefaultChannel = False
|
||||
ignoreChannels =
|
||||
# require ! to be the first character in a command
|
||||
cmdBang = False
|
||||
# require explicit command, the message will only be processed if it starts with a command word
|
||||
explicitCmd = True
|
||||
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
@@ -56,13 +58,15 @@ wikipedia = True
|
||||
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = False
|
||||
# Ollama model to use (defaults to gemma2:2b)
|
||||
# ollamaModel = llama3.1
|
||||
# Ollama model to use (defaults to gemma3:270m)
|
||||
# ollamaModel = gemma3:latest
|
||||
# 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
|
||||
# if True, the input is sent raw to the LLM, if False uses legacy template query
|
||||
rawLLMQuery = True
|
||||
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
@@ -87,6 +91,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
|
||||
@@ -105,13 +112,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
|
||||
@@ -144,8 +154,18 @@ NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
|
||||
# NOAA Hydrology unique identifiers, LID or USGS ID
|
||||
riverListDefault =
|
||||
# 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
|
||||
|
||||
# NOAA USGS Hydrology river identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverList =
|
||||
|
||||
# NOAA EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
@@ -159,16 +179,15 @@ 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 following 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
|
||||
@@ -292,10 +311,10 @@ 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
|
||||
|
||||
14
install.sh
14
install.sh
@@ -250,7 +250,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "ollama pull gemma2:2b\n"
|
||||
printf "ollama pull gemma3:latest\n"
|
||||
printf "Total download is multi GB, recomend pi5/8GB or better for this\n"
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
@@ -258,12 +258,12 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
if [[ $(echo "${ollama}" | grep -i "^y") ]]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
# ask if want to install gemma3:latest
|
||||
printf "\n Ollama install done now we can install the gemma3:latest components\n"
|
||||
echo "Do you want to install the gemma3:latest components? (y/n)"
|
||||
read gemma
|
||||
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
|
||||
ollama pull gemma2:2b
|
||||
ollama pull gemma3:latest
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -356,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
|
||||
|
||||
63
mesh_bot.py
63
mesh_bot.py
@@ -67,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!!🛜",
|
||||
@@ -750,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=coastalForecastDays)
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
@@ -1104,19 +1111,18 @@ def onReceive(packet, interface):
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
|
||||
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
|
||||
@@ -1156,10 +1162,12 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
rx_time = packet['decoded'].get('rxTime', time.time())
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay detected")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
@@ -1195,13 +1203,15 @@ def onReceive(packet, interface):
|
||||
|
||||
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")
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
@@ -1225,7 +1235,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
|
||||
@@ -1272,7 +1282,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):
|
||||
@@ -1398,6 +1409,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:
|
||||
@@ -1427,7 +1440,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:
|
||||
@@ -1548,10 +1564,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
|
||||
|
||||
174
modules/llm.py
174
modules/llm.py
@@ -8,32 +8,34 @@ from modules.log import *
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
import requests
|
||||
import json
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# This is my attempt at a simple RAG implementation it will require some setup
|
||||
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
|
||||
# This is lighter weight and can be used in a standalone environment, needs chromadb
|
||||
# "chat with a file" is the use concept here, the file is the RAG data
|
||||
# is anyone using this please let me know if you are Dec62024 -kelly
|
||||
ragDEV = False
|
||||
|
||||
if ragDEV:
|
||||
import os
|
||||
import ollama # pip install ollama
|
||||
import chromadb # pip install chromadb
|
||||
from ollama import Client as OllamaClient
|
||||
ollamaClient = OllamaClient(host=ollamaHostName)
|
||||
if not rawLLMQuery:
|
||||
# this may be removed in the future
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# LLM System Variables
|
||||
ollamaAPI = ollamaHostName + "/api/generate"
|
||||
tokens = 450 # max charcters for the LLM response, this is the max length of the response also in prompts
|
||||
requestTruncation = True # if True, the LLM "will" truncate the response
|
||||
|
||||
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
|
||||
|
||||
# Used in the meshBotAI template
|
||||
llmEnableHistory = True # enable last message history for the LLM model
|
||||
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
|
||||
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
antiFloodLLM = []
|
||||
llmChat_history = {}
|
||||
trap_list_llm = ("ask:", "askai")
|
||||
|
||||
meshbotAIinit = """
|
||||
keep responses as short as possible. chatbot assistant no followuyp questions, no asking for clarification.
|
||||
You must respond in plain text standard ASCII characters or emojis.
|
||||
"""
|
||||
|
||||
truncatePrompt = f"truncate this as short as possible:\n"
|
||||
|
||||
meshBotAI = """
|
||||
FROM {llmModel}
|
||||
SYSTEM
|
||||
@@ -74,78 +76,24 @@ if llmEnableHistory:
|
||||
|
||||
"""
|
||||
|
||||
def llm_readTextFiles():
|
||||
# read .txt files in ../data/rag
|
||||
try:
|
||||
text = []
|
||||
directory = "../data/rag"
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith(".txt"):
|
||||
filepath = os.path.join(directory, filename)
|
||||
with open(filepath, 'r') as f:
|
||||
text.append(f.read())
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM readTextFiles: {e}")
|
||||
return False
|
||||
|
||||
def store_text_embedding(text):
|
||||
try:
|
||||
# store each document in a vector embedding database
|
||||
for i, d in enumerate(text):
|
||||
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
|
||||
embedding = response["embedding"]
|
||||
collection.add(
|
||||
ids=[str(i)],
|
||||
embeddings=[embedding],
|
||||
documents=[d]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Embedding failed: {e}")
|
||||
return False
|
||||
|
||||
## INITALIZATION of RAG
|
||||
if ragDEV:
|
||||
try:
|
||||
chromaHostname = "localhost:8000"
|
||||
# connect to the chromaDB
|
||||
chromaHost = chromaHostname.split(":")[0]
|
||||
chromaPort = chromaHostname.split(":")[1]
|
||||
if chromaHost == "localhost" and chromaPort == "8000":
|
||||
# create a client using local python Client
|
||||
chromaClient = chromadb.Client()
|
||||
else:
|
||||
# create a client using the remote python Client
|
||||
# this isnt tested yet please test and report back
|
||||
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
|
||||
|
||||
clearCollection = False
|
||||
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
|
||||
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
|
||||
chromaClient.delete_collection("meshBotAI")
|
||||
|
||||
# create a new collection
|
||||
collection = chromaClient.create_collection("meshBotAI")
|
||||
|
||||
logger.debug(f"System: LLM: Cataloging RAG data")
|
||||
store_text_embedding(llm_readTextFiles())
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
|
||||
|
||||
def query_collection(prompt):
|
||||
# generate an embedding for the prompt and retrieve the most relevant doc
|
||||
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
|
||||
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
|
||||
data = results['documents'][0][0]
|
||||
return data
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
|
||||
# if this is the first initialization of the LLM the query of " " should bring meshbotAIinit OTA shouldnt reach this?
|
||||
# This is for LLM like gemma and others now?
|
||||
if input == " " and rawLLMQuery:
|
||||
logger.warning("System: These LLM models lack a traditional system prompt, they can be verbose and not very helpful be advised.")
|
||||
input = meshbotAIinit
|
||||
|
||||
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
|
||||
@@ -156,7 +104,7 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
else:
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
if llmContext_fromGoogle and not rawLLMQuery:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
|
||||
@@ -187,36 +135,29 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
|
||||
|
||||
try:
|
||||
# RAG context inclusion testing
|
||||
ragContext = False
|
||||
if ragDEV:
|
||||
ragContext = query_collection(input)
|
||||
|
||||
if ragContext:
|
||||
ragContextGooogle = ragContext + '\n'.join(googleResults)
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model with RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
# Condense the result to just needed
|
||||
if isinstance(result, dict):
|
||||
result = result.get("response")
|
||||
if rawLLMQuery:
|
||||
# sanitize the input to remove tool call syntax
|
||||
if '```' in input:
|
||||
logger.warning("System: LLM Query: Code markdown detected, removing for raw query")
|
||||
input = input.replace('```bash', '').replace('```python', '').replace('```', '')
|
||||
modelPrompt = input
|
||||
else:
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False}
|
||||
# Query the model via Ollama web API
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
# Condense the result to just needed
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
|
||||
# Query the model via Ollama web API
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
# Condense the result to just needed
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
@@ -225,6 +166,23 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
if rawLLMQuery and requestTruncation and len(response) > 450:
|
||||
#retryy loop to truncate the response
|
||||
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
|
||||
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
|
||||
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
|
||||
if truncateResult.status_code == 200:
|
||||
truncate_json = truncateResult.json()
|
||||
result = truncate_json.get("response", "")
|
||||
|
||||
else:
|
||||
#use the original result if truncation fails
|
||||
logger.warning("System: LLM Query: Truncation failed, using original response")
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
# done with the query, remove the user from the anti flood list
|
||||
antiFloodLLM.remove(nodeID)
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
@@ -453,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":
|
||||
@@ -472,8 +472,6 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# 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:
|
||||
@@ -491,10 +489,25 @@ 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 or not linked_data.text.strip():
|
||||
@@ -515,6 +528,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
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"
|
||||
@@ -532,32 +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}")
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
# add to alert list
|
||||
alerts.append({
|
||||
'alertType': alertType,
|
||||
'alertCode': alertCode,
|
||||
@@ -567,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]:
|
||||
@@ -684,4 +700,64 @@ 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_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pz_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_pz_data = marine_pz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pz_data.startswith("Expires:"):
|
||||
expires = marine_pz_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_pz_data.split("\n")
|
||||
marine_pz_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 pzDays blocks
|
||||
for block in day_blocks[:days]:
|
||||
marine_pz_report += block + "\n"
|
||||
|
||||
# remove last newline
|
||||
if marine_pz_report.endswith("\n"):
|
||||
marine_pz_report = marine_pz_report[:-1]
|
||||
|
||||
# remove NOAA EOF $$
|
||||
if marine_pz_report.endswith("$$"):
|
||||
marine_pz_report = marine_pz_report[:-2].strip()
|
||||
|
||||
# abbreviate the report
|
||||
marine_pz_report = abbreviate_noaa(marine_pz_report)
|
||||
if marine_pz_report == "":
|
||||
return NO_DATA_NOGPS
|
||||
return marine_pz_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
|
||||
@@ -198,6 +197,7 @@ try:
|
||||
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
cmdBang = config['general'].getboolean('cmdBang', False) # default off
|
||||
explicitCmd = config['general'].getboolean('explicitCmd', True) # default on
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
@@ -220,9 +220,11 @@ try:
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma3:270m') # default gemma3:270m
|
||||
rawLLMQuery = config['general'].getboolean('rawLLMQuery', True) #default True
|
||||
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
|
||||
@@ -239,6 +241,7 @@ try:
|
||||
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)
|
||||
@@ -249,7 +252,11 @@ try:
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
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
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default None
|
||||
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
|
||||
@@ -258,12 +265,12 @@ try:
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
|
||||
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
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
|
||||
@@ -356,7 +363,7 @@ try:
|
||||
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
|
||||
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
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
|
||||
except KeyError as e:
|
||||
|
||||
@@ -6,7 +6,7 @@ import requests # pip install requests
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
|
||||
@@ -63,7 +63,7 @@ def drap_xray_conditions():
|
||||
def get_sun(lat=0, lon=0):
|
||||
# get sunrise and sunset times using callers location or default
|
||||
obs = ephem.Observer()
|
||||
obs.date = datetime.now()
|
||||
obs.date = datetime.now(timezone.utc)
|
||||
sun = ephem.Sun()
|
||||
if lat != 0 and lon != 0:
|
||||
obs.lat = str(lat)
|
||||
@@ -74,9 +74,17 @@ def get_sun(lat=0, lon=0):
|
||||
|
||||
sun.compute(obs)
|
||||
sun_table = {}
|
||||
|
||||
# get the sun azimuth and altitude
|
||||
sun_table['azimuth'] = sun.az
|
||||
sun_table['altitude'] = sun.alt
|
||||
|
||||
# sun is up include altitude
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_table['altitude'] = sun.alt
|
||||
else:
|
||||
sun_table['altitude'] = 0
|
||||
|
||||
# get the next rise and set times
|
||||
local_sunrise = ephem.localtime(obs.next_rising(sun))
|
||||
local_sunset = ephem.localtime(obs.next_setting(sun))
|
||||
@@ -86,19 +94,25 @@ def get_sun(lat=0, lon=0):
|
||||
else:
|
||||
sun_table['rise_time'] = local_sunrise.strftime('%a %d %I:%M%p')
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
|
||||
# if sunset is before sunrise, then it's tomorrow
|
||||
|
||||
# if sunset is before sunrise, then data will be for tomorrow format sunset first and sunrise second
|
||||
if local_sunset < local_sunrise:
|
||||
local_sunset = ephem.localtime(obs.next_setting(sun)) + timedelta(1)
|
||||
if zuluTime:
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
|
||||
else:
|
||||
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
|
||||
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
|
||||
sun_data = "SunSet: " + sun_table['set_time'] + "\nRise: " + sun_table['rise_time']
|
||||
else:
|
||||
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
|
||||
|
||||
sun_data += "\nDaylight: " + str((local_sunset - local_sunrise).seconds // 3600) + "h " + str(((local_sunset - local_sunrise).seconds // 60) % 60) + "m"
|
||||
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_data += "\nRemaining: " + str((local_sunset - datetime.now()).seconds // 3600) + "h " + str(((local_sunset - datetime.now()).seconds // 60) % 60) + "m"
|
||||
|
||||
sun_data += "\nAzimuth: " + str('{0:.2f}'.format(sun_table['azimuth'] * 180 / ephem.pi)) + "°"
|
||||
if sun_table['altitude'] > 0:
|
||||
sun_data += "\nAltitude: " + str('{0:.2f}'.format(sun_table['altitude'] * 180 / ephem.pi)) + "°"
|
||||
return sun_data
|
||||
|
||||
def get_moon(lat=0, lon=0):
|
||||
# get moon phase and rise/set times using callers location or default
|
||||
# the phase calculation mght not be accurate (followup later)
|
||||
obs = ephem.Observer()
|
||||
moon = ephem.Moon()
|
||||
if lat != 0 and lon != 0:
|
||||
@@ -108,10 +122,28 @@ def get_moon(lat=0, lon=0):
|
||||
obs.lat = str(latitudeValue)
|
||||
obs.lon = str(longitudeValue)
|
||||
|
||||
obs.date = datetime.now()
|
||||
obs.date = datetime.now(timezone.utc)
|
||||
moon.compute(obs)
|
||||
moon_table = {}
|
||||
moon_phase = ['NewMoon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous', 'FullMoon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent'][round(moon.phase / (2 * ephem.pi) * 8) % 8]
|
||||
illum = moon.phase # 0 = new, 50 = first/last quarter, 100 = full
|
||||
|
||||
if illum < 1.0:
|
||||
moon_phase = 'New Moon🌑'
|
||||
elif illum < 49:
|
||||
moon_phase = 'Waxing Crescent🌒'
|
||||
elif 49 <= illum < 51:
|
||||
moon_phase = 'First Quarter🌓'
|
||||
elif illum < 99:
|
||||
moon_phase = 'Waxing Gibbous🌔'
|
||||
elif illum >= 99:
|
||||
moon_phase = 'Full Moon🌕'
|
||||
elif illum > 51:
|
||||
moon_phase = 'Waning Gibbous🌖'
|
||||
elif 51 >= illum > 49:
|
||||
moon_phase = 'Last Quarter🌗'
|
||||
else:
|
||||
moon_phase = 'Waning Crescent🌘'
|
||||
|
||||
moon_table['phase'] = moon_phase
|
||||
moon_table['illumination'] = moon.phase
|
||||
moon_table['azimuth'] = moon.az
|
||||
@@ -139,6 +171,11 @@ def get_moon(lat=0, lon=0):
|
||||
"\nPhase:" + moon_table['phase'] + " @:" + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
|
||||
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
|
||||
|
||||
# if moon is in the sky, add azimuth and altitude
|
||||
if moon_table['altitude'] > 0:
|
||||
moon_data += "\nAz: " + str('{0:.2f}'.format(moon_table['azimuth'] * 180 / ephem.pi)) + "°" + \
|
||||
"\nAlt: " + str('{0:.2f}'.format(moon_table['altitude'] * 180 / ephem.pi)) + "°"
|
||||
|
||||
return moon_data
|
||||
|
||||
def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
|
||||
@@ -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,29 @@ 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"
|
||||
|
||||
# USGS riverFlow Configuration
|
||||
if riverListDefault != ['']:
|
||||
help_message = help_message + ", riverflow"
|
||||
|
||||
# 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 +268,8 @@ 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
|
||||
myNodeNum1 = myNodeNum2 = myNodeNum3 = myNodeNum4 = myNodeNum5 = myNodeNum6 = myNodeNum7 = myNodeNum8 = myNodeNum9 = 777
|
||||
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:
|
||||
@@ -540,6 +555,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:
|
||||
@@ -663,11 +687,24 @@ def messageTrap(msg):
|
||||
message_list=msg.split(" ")
|
||||
for m in message_list:
|
||||
for t in trap_list:
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
return True
|
||||
if cmdBang and m.startswith("!"):
|
||||
return True
|
||||
if not explicitCmd:
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
if cmdBang:
|
||||
if m.startswith('!'):
|
||||
return True
|
||||
else:
|
||||
continue
|
||||
return True
|
||||
else:
|
||||
# if the index 0 of the message is a word in the trap list, return True
|
||||
if t.lower() == m.lower() and message_list.index(m) == 0:
|
||||
if cmdBang:
|
||||
if m.startswith('!'):
|
||||
return True
|
||||
else:
|
||||
continue
|
||||
return True
|
||||
# if no trap words found, run a search for near misses like ping? or cmd?
|
||||
for m in message_list:
|
||||
for t in range(len(trap_list)):
|
||||
@@ -779,7 +816,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)
|
||||
@@ -816,40 +853,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 = {}
|
||||
@@ -979,10 +985,11 @@ 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
|
||||
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled:
|
||||
# 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}")
|
||||
send_message(f"High Altitude {position_data['altitude']}m on Device:{rxNode} Node:{get_name_from_number(nodeID,'short',rxNode)}", highfly_channel, 0, rxNode)
|
||||
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 a maximum size of 20
|
||||
@@ -1128,21 +1135,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()
|
||||
|
||||
@@ -1152,15 +1164,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':
|
||||
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'host{nodeID}'))
|
||||
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}")
|
||||
@@ -1248,3 +1264,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)
|
||||
|
||||
44
pong_bot.py
44
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
|
||||
@@ -254,6 +254,7 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
@@ -283,10 +284,17 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
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:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
@@ -310,7 +318,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 +329,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 +452,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