mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
53 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 | ||
|
|
097cae6e94 |
179
README.md
179
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,6 +223,12 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
|
||||
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
|
||||
@@ -250,7 +331,7 @@ 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
|
||||
riverList = 14144700 # example Mouth of Columbia River
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
@@ -267,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)
|
||||
```
|
||||
|
||||
@@ -280,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
|
||||
@@ -392,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
|
||||
@@ -108,7 +112,8 @@ 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
|
||||
@@ -149,8 +154,18 @@ NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
25
mesh_bot.py
25
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,7 +1111,7 @@ 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 rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
@@ -1155,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'):
|
||||
@@ -1194,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:
|
||||
@@ -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:
|
||||
@@ -1431,8 +1444,6 @@ async def start_rx():
|
||||
# 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:
|
||||
|
||||
168
modules/llm.py
168
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,76 +76,16 @@ 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 "
|
||||
|
||||
@@ -162,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/
|
||||
|
||||
@@ -193,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:
|
||||
@@ -231,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":
|
||||
@@ -699,3 +699,65 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
# return the alerts
|
||||
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
|
||||
|
||||
|
||||
@@ -197,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
|
||||
@@ -219,8 +220,9 @@ 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
|
||||
@@ -250,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
|
||||
@@ -357,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,7 +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
|
||||
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 = 3
|
||||
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:
|
||||
@@ -541,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:
|
||||
@@ -664,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)):
|
||||
@@ -780,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)
|
||||
@@ -952,7 +988,8 @@ def consumeMetadata(packet, rxNode=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}")
|
||||
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
|
||||
@@ -1138,6 +1175,8 @@ async def retry_interface(nodeID):
|
||||
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}")
|
||||
|
||||
10
pong_bot.py
10
pong_bot.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user